From 79a317e3174fe1882e1d4d6103646994a018fadb Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Fri, 21 Aug 2026 20:51:40 +1000 Subject: [PATCH] Clear the tracked process after killing it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit KillProcesses disposes the diff tool's Process and left the reference on the TrackedMove. That is fine when the accept succeeds, because the entry goes; the problem is that it often does not. InnerMove kills first and then tries the move, and a locked target, the user choosing Ignore, or the eight retries running out all re-add the very same object to the dictionary. The entry is then pending with a disposed Process on it, and two paths read that property: the Accept-open hot key, which filters on Process is { HasExited: false }, and "Open diff tool" via DiffToolLauncher. Both throw InvalidOperationException, "No process is associated with this object", on the UI thread — where nothing is hooked to Application.ThreadException, so the tray takes the exception dialog. Null the property after disposing, which is what DiffToolLauncher already does in the same situation. --- .../TrackerLockedMoveTest.cs | 30 +++++++++++++++++++ src/DiffEngineTray/Tracker.cs | 6 ++++ 2 files changed, 36 insertions(+) diff --git a/src/DiffEngineTray.Tests/TrackerLockedMoveTest.cs b/src/DiffEngineTray.Tests/TrackerLockedMoveTest.cs index e3960fb8..7ece94cf 100644 --- a/src/DiffEngineTray.Tests/TrackerLockedMoveTest.cs +++ b/src/DiffEngineTray.Tests/TrackerLockedMoveTest.cs @@ -102,6 +102,36 @@ public async Task NoResolver_KeepsMovePending() await Assert.That(await File.ReadAllTextAsync(target1)).IsEqualTo("old"); } + /// + /// A move that fails is re-added, so whatever KillProcesses did to it has to leave it usable. + /// It used to leave a disposed Process on the entry, and the Accept-open hot key and + /// "Open diff tool" both read that property - throwing "No process is associated with this + /// object" on the UI thread, where nothing catches it. + /// + [Test] + public async Task Ignore_DropsTheKilledProcessFromThePendingMove() + { + await using var tracker = new RecordingTracker( + (_, _) => LockedFilesResponse.Ignore); + var lockProcess = FileLockUtils.StartFileLockProcess(target1); + // Stands in for the diff tool: a killable process the move is tracking + var toolProcess = FileLockUtils.StartFileLockProcess(temp2); + try + { + var tracked = tracker.AddMove(temp1, target1, "theExe", "theArguments", true, toolProcess.Id); + tracker.Accept(tracked); + + var pending = tracker.Moves.Single(); + await Assert.That(pending.Process).IsNull(); + // What the hot key and DiffToolLauncher do with it + await Assert.That(pending.Process is { HasExited: false }).IsFalse(); + } + finally + { + FileLockUtils.Cleanup(toolProcess); + FileLockUtils.Cleanup(lockProcess); + } + } static string CreateFile(string content) { var path = Path.Combine(Path.GetTempPath(), $"TrackerLockedMoveTest_{Guid.NewGuid()}.txt"); diff --git a/src/DiffEngineTray/Tracker.cs b/src/DiffEngineTray/Tracker.cs index 7a5a4523..5ab19434 100644 --- a/src/DiffEngineTray/Tracker.cs +++ b/src/DiffEngineTray/Tracker.cs @@ -645,6 +645,12 @@ static void KillProcesses(TrackedMove move) } move.Process.KillAndDispose(); + + // The move can come back: a locked target, the user picking Ignore, or the retries running + // out all re-add this same object. Leaving a disposed Process on it made the Accept-open + // hot key and "Open diff tool" throw "No process is associated with this object" on the UI + // thread, where nothing catches it + move.Process = null; } ///