diff --git a/src/DiffEngine.Tests/WindowsProcessTests.cs b/src/DiffEngine.Tests/WindowsProcessTests.cs index 0b86309d..b878eeae 100644 --- a/src/DiffEngine.Tests/WindowsProcessTests.cs +++ b/src/DiffEngine.Tests/WindowsProcessTests.cs @@ -53,4 +53,115 @@ public void FindAll_ReturnsProcessCommands() Debug.WriteLine($"{cmd.Process}: {cmd.Command}"); } } + + [Fact] + public void TryTerminateProcess_WithWindowedProcess_GracefullyCloses() + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return; + } + + // Start FakeDiffTool in windowed mode (has a main window) + var process = Process.Start(new ProcessStartInfo + { + FileName = FakeDiffTool.Exe, + Arguments = "--windowed", + UseShellExecute = false, + CreateNoWindow = true + }); + + Assert.NotNull(process); + + try + { + // Wait for the process to fully start and create its window + Thread.Sleep(1000); + + // Attempt graceful termination via CloseMainWindow + var result = WindowsProcess.TryTerminateProcess(process.Id); + + Assert.True(result); + + // Verify process exited gracefully + Assert.True(process.WaitForExit(1000)); + } + finally + { + // Cleanup: ensure process is killed if test fails + try + { + if (!process.HasExited) + { + process.Kill(); + } + } + catch + { + // Ignore cleanup errors + } + } + } + + [Fact] + public void TryTerminateProcess_WithNonWindowedProcess_ForcefullyTerminates() + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return; + } + + // Start FakeDiffTool - a console process without a main window + var process = Process.Start(new ProcessStartInfo + { + FileName = FakeDiffTool.Exe, + UseShellExecute = false, + CreateNoWindow = true + }); + + Assert.NotNull(process); + + try + { + // Wait for the process to fully start + Thread.Sleep(500); + + // Attempt termination (should fall back to forceful kill since no main window) + var result = WindowsProcess.TryTerminateProcess(process.Id); + + Assert.True(result); + + // Verify process was terminated (should be immediate with forceful kill) + Assert.True(process.WaitForExit(1000)); + } + finally + { + // Cleanup: ensure process is killed if test fails + try + { + if (!process.HasExited) + { + process.Kill(); + } + } + catch + { + // Ignore cleanup errors + } + } + } + + [Fact] + public void TryTerminateProcess_WithInvalidProcessId_ReturnsFalse() + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return; + } + + // Use a very unlikely process ID + var result = WindowsProcess.TryTerminateProcess(999999); + + Assert.False(result); + } } diff --git a/src/DiffEngine/Process/WindowsProcess.cs b/src/DiffEngine/Process/WindowsProcess.cs index bad583e3..d8b8b8ed 100644 --- a/src/DiffEngine/Process/WindowsProcess.cs +++ b/src/DiffEngine/Process/WindowsProcess.cs @@ -127,6 +127,28 @@ struct PROCESS_BASIC_INFORMATION public static bool TryTerminateProcess(int processId) { + // First, try graceful shutdown by closing the main window + try + { + using var process = System.Diagnostics.Process.GetProcessById(processId); + + // Try to close the main window gracefully + if (process.CloseMainWindow()) + { + // Wait up to 5 seconds for graceful exit + if (process.WaitForExit(5000)) + { + return true; + } + } + // If no main window or still running, fall through to force kill + } + catch + { + // Process may have already exited or be inaccessible, fall through to force kill + } + + // Fall back to forceful termination using var processHandle = OpenProcess(PROCESS_TERMINATE, false, processId); if (processHandle.IsInvalid) { diff --git a/src/Directory.Build.props b/src/Directory.Build.props index bae85037..4ebbca93 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -2,7 +2,7 @@ CS1591;CS0649;NU1608;NU1109 - 18.0.0 + 18.0.1 1.0.0 Testing, Snapshot, Diff, Compare Launches diff tools based on file extensions. Designed to be consumed by snapshot testing libraries. diff --git a/src/FakeDiffTool/FakeDiffTool.csproj b/src/FakeDiffTool/FakeDiffTool.csproj index 7f5d2b7e..7f17f42f 100644 --- a/src/FakeDiffTool/FakeDiffTool.csproj +++ b/src/FakeDiffTool/FakeDiffTool.csproj @@ -2,12 +2,13 @@ WinExe - net8.0 + net10.0-windows + true false bin win-x64 osx-x64 - true + false true true diff --git a/src/FakeDiffTool/Program.cs b/src/FakeDiffTool/Program.cs index 54e45c79..b0d9f35d 100644 --- a/src/FakeDiffTool/Program.cs +++ b/src/FakeDiffTool/Program.cs @@ -1,7 +1,28 @@ -using System.Threading; +using System; +using System.Threading; +using System.Windows.Forms; class Program { - static void Main() => - Thread.Sleep(5000); + [STAThread] + static void Main(string[] args) + { + // If --windowed is passed, create a simple form that can be closed gracefully + if (args.Length > 0 && args[0] == "--windowed") + { + Application.EnableVisualStyles(); + Application.SetCompatibleTextRenderingDefault(false); + Application.Run(new Form + { + Text = "FakeDiffTool", + WindowState = FormWindowState.Minimized, + ShowInTaskbar = false + }); + } + else + { + // Default behavior: just sleep (no main window) + Thread.Sleep(5000); + } + } } \ No newline at end of file