From 0d171e78f3a1f4662dfd6266a473d8d65154e14a Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Sat, 22 Aug 2026 13:18:28 +1000 Subject: [PATCH] Treat a missing ps as no running processes process.Start was unguarded, so a machine without ps raised Win32Exception, and a non-zero exit threw as well. Both propagate out of ProcessCleanup's static constructor, which turns them into a TypeInitializationException on every launch and every kill for the life of the process. A minimal container without procps is the ordinary way to get there, and one that does not set DOTNET_RUNNING_IN_CONTAINER gets no help from the disabled check either. The right answer is the one the timeout path already gives: no running processes, traced, carry on. Testable because Windows is a machine with no ps. The code is only used on Linux and macOS, but nothing about it refuses to run here, and here the executable is genuinely absent - so the test drives the real failure rather than a simulation of it. It fails without the guard. --- src/DiffEngine.Tests/PsAbsentTests.cs | 25 ++++++++++++++++ src/DiffEngine/Process/LinuxOsxProcess.cs | 35 ++++++++++++++++++----- 2 files changed, 53 insertions(+), 7 deletions(-) create mode 100644 src/DiffEngine.Tests/PsAbsentTests.cs diff --git a/src/DiffEngine.Tests/PsAbsentTests.cs b/src/DiffEngine.Tests/PsAbsentTests.cs new file mode 100644 index 00000000..c9af77bf --- /dev/null +++ b/src/DiffEngine.Tests/PsAbsentTests.cs @@ -0,0 +1,25 @@ +/// +/// A machine with no ps. +/// +/// process.Start was unguarded and a non-zero exit threw, and both propagate out of +/// ProcessCleanup's static constructor - so a minimal container without procps, which is also one +/// that does not set DOTNET_RUNNING_IN_CONTAINER, got a permanent TypeInitializationException on +/// every launch and kill rather than "no running processes". The timeout path already degraded. +/// +/// +/// Windows is the machine with no ps, which is what makes this testable at all: the code is only +/// used on Linux and macOS, but nothing about it refuses to run here, and here the executable is +/// genuinely missing. +/// +/// +[RunOn(TUnit.Core.Enums.OS.Windows)] +public class PsAbsentTests +{ + [Test] + public async Task NoPsMeansNoProcessesRatherThanAThrow() + { + var commands = LinuxOsxProcess.FindAll(); + + await Assert.That(commands).IsEmpty(); + } +} diff --git a/src/DiffEngine/Process/LinuxOsxProcess.cs b/src/DiffEngine/Process/LinuxOsxProcess.cs index ba0a3c4f..ad48aeb7 100644 --- a/src/DiffEngine/Process/LinuxOsxProcess.cs +++ b/src/DiffEngine/Process/LinuxOsxProcess.cs @@ -107,7 +107,24 @@ static bool TryRunPs([NotNullWhen(true)] out string? result) CreateNoWindow = false } }; - process.Start(); + try + { + process.Start(); + } + catch (Exception exception) + when (exception is System.ComponentModel.Win32Exception or InvalidOperationException) + { + // No ps on this machine. A minimal container without procps is the ordinary case, and + // one that does not set DOTNET_RUNNING_IN_CONTAINER gets this far. Degrading to "no + // running processes" is what the timeout below already does, and the alternative is + // far worse than a wrong answer: this runs from ProcessCleanup's static constructor, + // so it becomes a TypeInitializationException on every launch and kill for the life + // of the process + Trace.WriteLine($"DiffEngine: Could not start ps. Treating as no running processes. {exception.Message}"); + result = null; + return false; + } + process.OutputDataReceived += (_, args) => { outputBuilder.AppendLine(args.Data); @@ -127,12 +144,16 @@ static bool TryRunPs([NotNullWhen(true)] out string? result) if (process.ExitCode != 0) { - var error = $""" - Could not execute process. Command line: ps {arguments}. - Output: {outputBuilder} - Error: {errorBuilder} - """; - throw new(error); + // Reported rather than thrown, for the same reason a failure to start is: the caller + // is a static constructor, and a throw there is permanent for the process + Trace.WriteLine( + $""" + DiffEngine: ps exited with {process.ExitCode}. Treating as no running processes. Command line: ps {arguments}. + Output: {outputBuilder} + Error: {errorBuilder} + """); + result = null; + return false; } result = outputBuilder.ToString();