Skip to content

Commit 8b7c99d

Browse files
authored
Make try terminate process more graceful (#668)
1 parent c8dbd28 commit 8b7c99d

File tree

5 files changed

+161
-6
lines changed

5 files changed

+161
-6
lines changed

src/DiffEngine.Tests/WindowsProcessTests.cs

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,115 @@ public void FindAll_ReturnsProcessCommands()
5353
Debug.WriteLine($"{cmd.Process}: {cmd.Command}");
5454
}
5555
}
56+
57+
[Fact]
58+
public void TryTerminateProcess_WithWindowedProcess_GracefullyCloses()
59+
{
60+
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
61+
{
62+
return;
63+
}
64+
65+
// Start FakeDiffTool in windowed mode (has a main window)
66+
var process = Process.Start(new ProcessStartInfo
67+
{
68+
FileName = FakeDiffTool.Exe,
69+
Arguments = "--windowed",
70+
UseShellExecute = false,
71+
CreateNoWindow = true
72+
});
73+
74+
Assert.NotNull(process);
75+
76+
try
77+
{
78+
// Wait for the process to fully start and create its window
79+
Thread.Sleep(1000);
80+
81+
// Attempt graceful termination via CloseMainWindow
82+
var result = WindowsProcess.TryTerminateProcess(process.Id);
83+
84+
Assert.True(result);
85+
86+
// Verify process exited gracefully
87+
Assert.True(process.WaitForExit(1000));
88+
}
89+
finally
90+
{
91+
// Cleanup: ensure process is killed if test fails
92+
try
93+
{
94+
if (!process.HasExited)
95+
{
96+
process.Kill();
97+
}
98+
}
99+
catch
100+
{
101+
// Ignore cleanup errors
102+
}
103+
}
104+
}
105+
106+
[Fact]
107+
public void TryTerminateProcess_WithNonWindowedProcess_ForcefullyTerminates()
108+
{
109+
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
110+
{
111+
return;
112+
}
113+
114+
// Start FakeDiffTool - a console process without a main window
115+
var process = Process.Start(new ProcessStartInfo
116+
{
117+
FileName = FakeDiffTool.Exe,
118+
UseShellExecute = false,
119+
CreateNoWindow = true
120+
});
121+
122+
Assert.NotNull(process);
123+
124+
try
125+
{
126+
// Wait for the process to fully start
127+
Thread.Sleep(500);
128+
129+
// Attempt termination (should fall back to forceful kill since no main window)
130+
var result = WindowsProcess.TryTerminateProcess(process.Id);
131+
132+
Assert.True(result);
133+
134+
// Verify process was terminated (should be immediate with forceful kill)
135+
Assert.True(process.WaitForExit(1000));
136+
}
137+
finally
138+
{
139+
// Cleanup: ensure process is killed if test fails
140+
try
141+
{
142+
if (!process.HasExited)
143+
{
144+
process.Kill();
145+
}
146+
}
147+
catch
148+
{
149+
// Ignore cleanup errors
150+
}
151+
}
152+
}
153+
154+
[Fact]
155+
public void TryTerminateProcess_WithInvalidProcessId_ReturnsFalse()
156+
{
157+
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
158+
{
159+
return;
160+
}
161+
162+
// Use a very unlikely process ID
163+
var result = WindowsProcess.TryTerminateProcess(999999);
164+
165+
Assert.False(result);
166+
}
56167
}

src/DiffEngine/Process/WindowsProcess.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,28 @@ struct PROCESS_BASIC_INFORMATION
127127

128128
public static bool TryTerminateProcess(int processId)
129129
{
130+
// First, try graceful shutdown by closing the main window
131+
try
132+
{
133+
using var process = System.Diagnostics.Process.GetProcessById(processId);
134+
135+
// Try to close the main window gracefully
136+
if (process.CloseMainWindow())
137+
{
138+
// Wait up to 5 seconds for graceful exit
139+
if (process.WaitForExit(5000))
140+
{
141+
return true;
142+
}
143+
}
144+
// If no main window or still running, fall through to force kill
145+
}
146+
catch
147+
{
148+
// Process may have already exited or be inaccessible, fall through to force kill
149+
}
150+
151+
// Fall back to forceful termination
130152
using var processHandle = OpenProcess(PROCESS_TERMINATE, false, processId);
131153
if (processHandle.IsInvalid)
132154
{

src/Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<Project>
33
<PropertyGroup>
44
<NoWarn>CS1591;CS0649;NU1608;NU1109</NoWarn>
5-
<Version>18.0.0</Version>
5+
<Version>18.0.1</Version>
66
<AssemblyVersion>1.0.0</AssemblyVersion>
77
<PackageTags>Testing, Snapshot, Diff, Compare</PackageTags>
88
<Description>Launches diff tools based on file extensions. Designed to be consumed by snapshot testing libraries.</Description>

src/FakeDiffTool/FakeDiffTool.csproj

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@
22

33
<PropertyGroup>
44
<OutputType>WinExe</OutputType>
5-
<TargetFramework>net8.0</TargetFramework>
5+
<TargetFramework>net10.0-windows</TargetFramework>
6+
<UseWindowsForms>true</UseWindowsForms>
67
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
78
<OutputPath>bin</OutputPath>
89
<RuntimeIdentifier Condition=" '$(OS)' == 'Windows_NT' ">win-x64</RuntimeIdentifier>
910
<RuntimeIdentifier Condition=" '$(OS)' != 'Windows_NT' ">osx-x64</RuntimeIdentifier>
10-
<PublishTrimmed>true</PublishTrimmed>
11+
<PublishTrimmed>false</PublishTrimmed>
1112
<PublishReadyToRun>true</PublishReadyToRun>
1213
<PublishSingleFile>true</PublishSingleFile>
1314
</PropertyGroup>

src/FakeDiffTool/Program.cs

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,28 @@
1-
using System.Threading;
1+
using System;
2+
using System.Threading;
3+
using System.Windows.Forms;
24

35
class Program
46
{
5-
static void Main() =>
6-
Thread.Sleep(5000);
7+
[STAThread]
8+
static void Main(string[] args)
9+
{
10+
// If --windowed is passed, create a simple form that can be closed gracefully
11+
if (args.Length > 0 && args[0] == "--windowed")
12+
{
13+
Application.EnableVisualStyles();
14+
Application.SetCompatibleTextRenderingDefault(false);
15+
Application.Run(new Form
16+
{
17+
Text = "FakeDiffTool",
18+
WindowState = FormWindowState.Minimized,
19+
ShowInTaskbar = false
20+
});
21+
}
22+
else
23+
{
24+
// Default behavior: just sleep (no main window)
25+
Thread.Sleep(5000);
26+
}
27+
}
728
}

0 commit comments

Comments
 (0)