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
111 changes: 111 additions & 0 deletions src/DiffEngine.Tests/WindowsProcessTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
22 changes: 22 additions & 0 deletions src/DiffEngine/Process/WindowsProcess.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down
2 changes: 1 addition & 1 deletion src/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<Project>
<PropertyGroup>
<NoWarn>CS1591;CS0649;NU1608;NU1109</NoWarn>
<Version>18.0.0</Version>
<Version>18.0.1</Version>
<AssemblyVersion>1.0.0</AssemblyVersion>
<PackageTags>Testing, Snapshot, Diff, Compare</PackageTags>
<Description>Launches diff tools based on file extensions. Designed to be consumed by snapshot testing libraries.</Description>
Expand Down
5 changes: 3 additions & 2 deletions src/FakeDiffTool/FakeDiffTool.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@

<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net10.0-windows</TargetFramework>
<UseWindowsForms>true</UseWindowsForms>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<OutputPath>bin</OutputPath>
<RuntimeIdentifier Condition=" '$(OS)' == 'Windows_NT' ">win-x64</RuntimeIdentifier>
<RuntimeIdentifier Condition=" '$(OS)' != 'Windows_NT' ">osx-x64</RuntimeIdentifier>
<PublishTrimmed>true</PublishTrimmed>
<PublishTrimmed>false</PublishTrimmed>
<PublishReadyToRun>true</PublishReadyToRun>
<PublishSingleFile>true</PublishSingleFile>
</PropertyGroup>
Expand Down
27 changes: 24 additions & 3 deletions src/FakeDiffTool/Program.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
}