Skip to content

Commit 8ee659d

Browse files
committed
Hold a handle on a tracked process
Process.GetProcessById holds no OS handle, so Kill, HasExited and MainWindowHandle each re-open the id at the moment they are called. A tracked move outlives its diff tool by design - HandleScanMove keeps it while the temp file is still there - so hours later that id may belong to something else entirely, and "Accept all" or "Open diff tool" would act on whatever that is. Touching Handle at track time opens one and keeps it, and Windows will not hand out an id while a handle to it exists, so there is nothing left to confuse the tracked process with. When the handle cannot be held - it exited between the probe and here, or this account cannot open it - TryGet now reports no process rather than one it cannot identify. The tool then goes unkilled, which is better than something else being killed in its place. The test asserts through the exit code, which is readable only if a handle was open before the process ended. That is the same fact as the id being held.
1 parent 0b71112 commit 8ee659d

2 files changed

Lines changed: 75 additions & 1 deletion

File tree

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/// <summary>
2+
/// TryGet holds an OS handle on the process it hands back.
3+
/// <para>
4+
/// Process.GetProcessById holds none of its own, so Kill, HasExited and MainWindowHandle each
5+
/// re-open the id at the moment they are called. A tracked move outlives its diff tool by design -
6+
/// HandleScanMove keeps it while the temp file is still there - so hours later that id may belong
7+
/// to something else, and "Accept all" or "Open diff tool" would act on whatever it is.
8+
/// </para>
9+
/// <para>
10+
/// Asserted through the exit code, which is only readable if a handle was open before the process
11+
/// ended. That is the same thing as the id being held, since Windows will not hand out an id while
12+
/// a handle to it exists.
13+
/// </para>
14+
/// </summary>
15+
[NotInParallel]
16+
public class ProcessExHandleTests
17+
{
18+
[Test]
19+
public async Task TheProcessIsStillReadableAfterItExits()
20+
{
21+
// Any long lived process will do; this project has no fake tool of its own
22+
var started = FileLockUtils.StartFileLockProcess(Path.GetTempFileName());
23+
24+
try
25+
{
26+
await Assert.That(ProcessEx.TryGet(started.Id, out var tracked)).IsTrue();
27+
28+
started.Kill();
29+
await Assert.That(started.WaitForExit(30000)).IsTrue();
30+
31+
// Readable only because TryGet opened a handle while it was still running
32+
await Assert.That(tracked!.HasExited).IsTrue();
33+
await Assert.That(tracked.ExitCode).IsNotEqualTo(int.MinValue);
34+
tracked.Dispose();
35+
}
36+
finally
37+
{
38+
try
39+
{
40+
if (!started.HasExited)
41+
{
42+
started.Kill();
43+
}
44+
}
45+
catch
46+
{
47+
// Already gone
48+
}
49+
50+
started.Dispose();
51+
}
52+
}
53+
}

‎src/DiffEngineTray/ProcessEx.cs‎

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,19 @@ public static bool TryGet(int id, [NotNullWhen(true)] out Process? process)
1616
}
1717
}
1818

19+
Process? opened = null;
1920
try
2021
{
21-
process = Process.GetProcessById(id);
22+
opened = Process.GetProcessById(id);
23+
process = opened;
24+
// Forces the OS handle open, and keeps it. GetProcessById holds none of its own, so
25+
// Kill, HasExited and MainWindowHandle each re-open the id at the moment they are
26+
// called - and a tracked move outlives its diff tool by design, since HandleScanMove
27+
// keeps it while the temp file is still there. Hours later that id may belong to
28+
// something else, and "Accept all" or "Open diff tool" would kill whatever it is.
29+
// An open handle also stops Windows handing the id out again while this move is
30+
// tracked, so there is nothing to confuse it with
31+
_ = process.Handle;
2232
return true;
2333
}
2434
catch (ArgumentException)
@@ -27,6 +37,17 @@ public static bool TryGet(int id, [NotNullWhen(true)] out Process? process)
2737
process = null;
2838
return false;
2939
}
40+
catch (Exception exception)
41+
when (exception is Win32Exception or InvalidOperationException)
42+
{
43+
// The handle could not be held - it exited between the probe above and here, or this
44+
// account cannot open it. Without one there is no way to tell the process apart from a
45+
// later holder of the same id, so it is better tracked as no process at all: the tool
46+
// is then not killed, rather than something else being killed in its place
47+
opened?.Dispose();
48+
process = null;
49+
return false;
50+
}
3051
}
3152

3253
public static void KillAndDispose(this Process process)

0 commit comments

Comments
 (0)