diff --git a/src/DiffEngine.Tests/GlobalUsings.cs b/src/DiffEngine.Tests/GlobalUsings.cs
index 1fb97b26..85c75b44 100644
--- a/src/DiffEngine.Tests/GlobalUsings.cs
+++ b/src/DiffEngine.Tests/GlobalUsings.cs
@@ -1 +1 @@
-global using EmptyFiles;
\ No newline at end of file
+global using EmptyFiles;
\ No newline at end of file
diff --git a/src/DiffEngine.Tests/WindowsProcessTests.cs b/src/DiffEngine.Tests/WindowsProcessTests.cs
new file mode 100644
index 00000000..0b86309d
--- /dev/null
+++ b/src/DiffEngine.Tests/WindowsProcessTests.cs
@@ -0,0 +1,56 @@
+#if NET5_0_OR_GREATER
+[System.Runtime.Versioning.SupportedOSPlatform("windows")]
+#endif
+public class WindowsProcessTests(ITestOutputHelper output) :
+ XunitContextBase(output)
+{
+ [Theory]
+ [InlineData("\"C:\\Program Files\\Beyond Compare 4\\BComp.exe\" C:\\temp\\file.1.txt C:\\temp\\file.2.txt", true)]
+ [InlineData("notepad.exe C:\\Users\\test\\doc.1.txt C:\\Users\\test\\doc.2.txt", true)]
+ [InlineData("\"C:\\diff\\tool.exe\" D:\\path\\to\\source.1.cs D:\\path\\to\\target.2.cs", true)]
+ [InlineData("code.exe --diff file.a.b file.c.d", true)]
+ [InlineData("app.exe path.with.dots path.more.dots", true)]
+ public void MatchesPattern_WithTwoFilePaths_ReturnsTrue(string commandLine, bool expected)
+ {
+ if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ return;
+ }
+
+ Assert.Equal(expected, WindowsProcess.MatchesPattern(commandLine));
+ }
+
+ [Theory]
+ [InlineData("notepad.exe")]
+ [InlineData("notepad.exe C:\\temp\\file.txt")]
+ [InlineData("cmd.exe /c dir")]
+ [InlineData("explorer.exe")]
+ [InlineData("")]
+ [InlineData("singleword")]
+ [InlineData("app.exe onepath.with.dots")]
+ public void MatchesPattern_WithoutTwoFilePaths_ReturnsFalse(string commandLine)
+ {
+ if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ return;
+ }
+
+ Assert.False(WindowsProcess.MatchesPattern(commandLine));
+ }
+
+ [Fact]
+ public void FindAll_ReturnsProcessCommands()
+ {
+ if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ return;
+ }
+
+ var result = WindowsProcess.FindAll();
+ Assert.NotNull(result);
+ foreach (var cmd in result)
+ {
+ Debug.WriteLine($"{cmd.Process}: {cmd.Command}");
+ }
+ }
+}
diff --git a/src/DiffEngine/DiffEngine.csproj b/src/DiffEngine/DiffEngine.csproj
index 185e5838..2a93061b 100644
--- a/src/DiffEngine/DiffEngine.csproj
+++ b/src/DiffEngine/DiffEngine.csproj
@@ -2,10 +2,11 @@
net462;net472;net48;net9.0;net10.0
$(TargetFrameworks);net6.0;net7.0;net8.0;net9.0;net10.0
+ true
-
+
@@ -19,10 +20,6 @@
-
-
-
-
diff --git a/src/DiffEngine/Process/WindowsProcess.cs b/src/DiffEngine/Process/WindowsProcess.cs
index 80853b82..0ab9063a 100644
--- a/src/DiffEngine/Process/WindowsProcess.cs
+++ b/src/DiffEngine/Process/WindowsProcess.cs
@@ -1,19 +1,135 @@
-static class WindowsProcess
+using System.Text;
+
+static partial class WindowsProcess
{
- [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
+#if NET7_0_OR_GREATER
+ [LibraryImport("kernel32.dll", SetLastError = true)]
+ private static partial SafeProcessHandle OpenProcess(
+ int access,
+ [MarshalAs(UnmanagedType.Bool)] bool inherit,
+ int processId);
+
+ [LibraryImport("kernel32.dll", SetLastError = true)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ private static partial bool TerminateProcess(
+ SafeProcessHandle processHandle,
+ int exitCode);
+
+ [LibraryImport("kernel32.dll", SetLastError = true)]
+ private static partial IntPtr CreateToolhelp32Snapshot(uint flags, uint processId);
+
+ [LibraryImport("kernel32.dll", SetLastError = true)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ private static partial bool CloseHandle(IntPtr handle);
+
+ [LibraryImport("ntdll.dll")]
+ private static partial int NtQueryInformationProcess(
+ SafeProcessHandle handle,
+ int processInformationClass,
+ ref PROCESS_BASIC_INFORMATION info,
+ int size,
+ out int returnLength);
+
+ [LibraryImport("kernel32.dll", SetLastError = true)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ private static partial bool IsWow64Process(
+ SafeProcessHandle handle,
+ [MarshalAs(UnmanagedType.Bool)] out bool isWow64);
+
+ // These methods use complex marshalling not supported by LibraryImport
+ [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
+ static extern bool Process32FirstW(IntPtr snapshot, ref PROCESSENTRY32W entry);
+
+ [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
+ static extern bool Process32NextW(IntPtr snapshot, ref PROCESSENTRY32W entry);
+
+ [DllImport("kernel32.dll", SetLastError = true)]
+ static extern bool ReadProcessMemory(
+ SafeProcessHandle handle,
+ IntPtr baseAddress,
+ [Out] byte[] buffer,
+ IntPtr size,
+ out IntPtr bytesRead);
+#else
+ [DllImport("kernel32.dll", SetLastError = true)]
static extern SafeProcessHandle OpenProcess(
int access,
bool inherit,
int processId);
- [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
+ [DllImport("kernel32.dll", SetLastError = true)]
static extern bool TerminateProcess(
SafeProcessHandle processHandle,
int exitCode);
+ [DllImport("kernel32.dll", SetLastError = true)]
+ static extern IntPtr CreateToolhelp32Snapshot(uint flags, uint processId);
+
+ [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
+ static extern bool Process32FirstW(IntPtr snapshot, ref PROCESSENTRY32W entry);
+
+ [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
+ static extern bool Process32NextW(IntPtr snapshot, ref PROCESSENTRY32W entry);
+
+ [DllImport("kernel32.dll", SetLastError = true)]
+ static extern bool CloseHandle(IntPtr handle);
+
+ [DllImport("ntdll.dll")]
+ static extern int NtQueryInformationProcess(
+ SafeProcessHandle handle,
+ int processInformationClass,
+ ref PROCESS_BASIC_INFORMATION info,
+ int size,
+ out int returnLength);
+
+ [DllImport("kernel32.dll", SetLastError = true)]
+ static extern bool ReadProcessMemory(
+ SafeProcessHandle handle,
+ IntPtr baseAddress,
+ [Out] byte[] buffer,
+ IntPtr size,
+ out IntPtr bytesRead);
+
+ [DllImport("kernel32.dll", SetLastError = true)]
+ static extern bool IsWow64Process(SafeProcessHandle handle, out bool isWow64);
+#endif
+
+ const uint TH32CS_SNAPPROCESS = 0x00000002;
+ const int PROCESS_QUERY_INFORMATION = 0x0400;
+ const int PROCESS_VM_READ = 0x0010;
+ const int PROCESS_TERMINATE = 0x0001;
+ const int ProcessBasicInformation = 0;
+
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
+ struct PROCESSENTRY32W
+ {
+ public uint dwSize;
+ public uint cntUsage;
+ public uint th32ProcessID;
+ public IntPtr th32DefaultHeapID;
+ public uint th32ModuleID;
+ public uint cntThreads;
+ public uint th32ParentProcessID;
+ public int pcPriClassBase;
+ public uint dwFlags;
+ [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
+ public string szExeFile;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ struct PROCESS_BASIC_INFORMATION
+ {
+ public IntPtr Reserved1;
+ public IntPtr PebBaseAddress;
+ public IntPtr Reserved2_0;
+ public IntPtr Reserved2_1;
+ public IntPtr UniqueProcessId;
+ public IntPtr Reserved3;
+ }
+
public static bool TryTerminateProcess(int processId)
{
- using var processHandle = OpenProcess(4097, false, processId);
+ using var processHandle = OpenProcess(PROCESS_TERMINATE, false, processId);
if (processHandle.IsInvalid)
{
return false;
@@ -26,22 +142,222 @@ public static bool TryTerminateProcess(int processId)
public static List FindAll()
{
var commands = new List();
- const string query =
- """
- select CommandLine, ProcessId
- from Win32_Process
- where CommandLine like '% %.%.% %.%.%'
- """;
- using var searcher = new ManagementObjectSearcher(query);
- using var collection = searcher.Get();
- foreach (var process in collection)
- {
- var command = (string) process["CommandLine"];
- var id = (int) Convert.ChangeType(process["ProcessId"], typeof(int));
- process.Dispose();
- commands.Add(new(command, id));
+ var snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
+
+ if (snapshot == IntPtr.Zero || snapshot == new IntPtr(-1))
+ {
+ return commands;
+ }
+
+ try
+ {
+ var entry = new PROCESSENTRY32W { dwSize = (uint)Marshal.SizeOf() };
+
+ if (!Process32FirstW(snapshot, ref entry))
+ {
+ return commands;
+ }
+
+ do
+ {
+ var processId = (int)entry.th32ProcessID;
+ if (processId == 0)
+ {
+ continue;
+ }
+
+ var commandLine = GetCommandLine(processId);
+ if (commandLine != null && MatchesPattern(commandLine))
+ {
+ commands.Add(new(commandLine, processId));
+ }
+ }
+ while (Process32NextW(snapshot, ref entry));
+ }
+ finally
+ {
+ CloseHandle(snapshot);
}
return commands;
}
-}
\ No newline at end of file
+
+ // Pattern: '% %.%.% %.%.%' - matches command lines with two file paths
+ // Each path has at least one space, then path separators (like C:\foo\bar.txt)
+ internal static bool MatchesPattern(string commandLine)
+ {
+ // Looking for pattern with path separators (backslash or forward slash)
+ // The WMI pattern '% %.%.% %.%.%' looks for:
+ // - anything, space, something.something.something, space, something.something.something
+ // This typically matches: "program.exe path\to\file.ext path\to\file2.ext"
+ var span = commandLine.AsSpan();
+ var firstDotPath = FindDotSeparatedPath(span);
+ if (firstDotPath < 0)
+ {
+ return false;
+ }
+
+ var remaining = span[(firstDotPath + 1)..];
+ var spaceAfterFirst = remaining.IndexOf(' ');
+ if (spaceAfterFirst < 0)
+ {
+ return false;
+ }
+
+ remaining = remaining[(spaceAfterFirst + 1)..];
+ return FindDotSeparatedPath(remaining) >= 0;
+ }
+
+ static int FindDotSeparatedPath(CharSpan span)
+ {
+ // Look for pattern like: x.x.x (at least 2 dots with content between)
+ var dotCount = 0;
+ var lastDot = -1;
+ for (var i = 0; i < span.Length; i++)
+ {
+ if (span[i] == '.')
+ {
+ if (lastDot >= 0 && i - lastDot > 1)
+ {
+ dotCount++;
+ if (dotCount >= 2)
+ {
+ return i;
+ }
+ }
+ else if (lastDot < 0)
+ {
+ dotCount = 1;
+ }
+
+ lastDot = i;
+ }
+ else if (span[i] == ' ')
+ {
+ dotCount = 0;
+ lastDot = -1;
+ }
+ }
+
+ return -1;
+ }
+
+ static string? GetCommandLine(int processId)
+ {
+ using var handle = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, false, processId);
+ if (handle.IsInvalid)
+ {
+ return null;
+ }
+
+ try
+ {
+ var isTarget32Bit = false;
+ if (Environment.Is64BitOperatingSystem)
+ {
+ if (!IsWow64Process(handle, out isTarget32Bit))
+ {
+ return null;
+ }
+ }
+
+ var pbi = new PROCESS_BASIC_INFORMATION();
+ if (NtQueryInformationProcess(handle, ProcessBasicInformation, ref pbi, Marshal.SizeOf(pbi), out _) != 0)
+ {
+ return null;
+ }
+
+ return Environment.Is64BitProcess && !isTarget32Bit
+ ? ReadCommandLine64(handle, pbi.PebBaseAddress)
+ : ReadCommandLine32(handle, pbi.PebBaseAddress);
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ static string? ReadCommandLine64(SafeProcessHandle handle, IntPtr pebAddress)
+ {
+ // In 64-bit PEB, ProcessParameters is at offset 0x20
+ var processParametersOffset = 0x20;
+ var commandLineOffset = 0x70; // UNICODE_STRING CommandLine in RTL_USER_PROCESS_PARAMETERS
+
+ var buffer = new byte[8];
+ if (!ReadProcessMemory(handle, pebAddress + processParametersOffset, buffer, new IntPtr(8), out _))
+ {
+ return null;
+ }
+
+ var processParameters = (IntPtr)BitConverter.ToInt64(buffer, 0);
+ if (processParameters == IntPtr.Zero)
+ {
+ return null;
+ }
+
+ // Read UNICODE_STRING structure (Length: 2, MaxLength: 2, padding: 4, Buffer: 8)
+ buffer = new byte[16];
+ if (!ReadProcessMemory(handle, processParameters + commandLineOffset, buffer, new IntPtr(16), out _))
+ {
+ return null;
+ }
+
+ var length = BitConverter.ToUInt16(buffer, 0);
+ var cmdLinePtr = (IntPtr)BitConverter.ToInt64(buffer, 8);
+
+ if (length == 0 || cmdLinePtr == IntPtr.Zero)
+ {
+ return null;
+ }
+
+ var cmdLineBuffer = new byte[length];
+ if (!ReadProcessMemory(handle, cmdLinePtr, cmdLineBuffer, new IntPtr(length), out _))
+ {
+ return null;
+ }
+
+ return Encoding.Unicode.GetString(cmdLineBuffer).TrimEnd('\0');
+ }
+
+ static string? ReadCommandLine32(SafeProcessHandle handle, IntPtr pebAddress)
+ {
+ // In 32-bit PEB, ProcessParameters is at offset 0x10
+ var processParametersOffset = 0x10;
+ var commandLineOffset = 0x40; // UNICODE_STRING CommandLine in RTL_USER_PROCESS_PARAMETERS
+
+ var buffer = new byte[4];
+ if (!ReadProcessMemory(handle, pebAddress + processParametersOffset, buffer, new IntPtr(4), out _))
+ {
+ return null;
+ }
+
+ var processParameters = (IntPtr)BitConverter.ToInt32(buffer, 0);
+ if (processParameters == IntPtr.Zero)
+ {
+ return null;
+ }
+
+ // Read UNICODE_STRING structure (Length: 2, MaxLength: 2, Buffer: 4)
+ buffer = new byte[8];
+ if (!ReadProcessMemory(handle, processParameters + commandLineOffset, buffer, new IntPtr(8), out _))
+ {
+ return null;
+ }
+
+ var length = BitConverter.ToUInt16(buffer, 0);
+ var cmdLinePtr = (IntPtr)BitConverter.ToInt32(buffer, 4);
+
+ if (length == 0 || cmdLinePtr == IntPtr.Zero)
+ {
+ return null;
+ }
+
+ var cmdLineBuffer = new byte[length];
+ if (!ReadProcessMemory(handle, cmdLinePtr, cmdLineBuffer, new IntPtr(length), out _))
+ {
+ return null;
+ }
+
+ return Encoding.Unicode.GetString(cmdLineBuffer).TrimEnd('\0');
+ }
+}
diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props
index ba54caa3..36cdab66 100644
--- a/src/Directory.Packages.props
+++ b/src/Directory.Packages.props
@@ -16,7 +16,6 @@
-