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
25 changes: 25 additions & 0 deletions src/DiffEngine.Tests/PsAbsentTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/// <summary>
/// A machine with no ps.
/// <para>
/// 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.
/// </para>
/// <para>
/// 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.
/// </para>
/// </summary>
[RunOn(TUnit.Core.Enums.OS.Windows)]
public class PsAbsentTests
{
[Test]
public async Task NoPsMeansNoProcessesRatherThanAThrow()
{
var commands = LinuxOsxProcess.FindAll();

await Assert.That(commands).IsEmpty();
}
}
35 changes: 28 additions & 7 deletions src/DiffEngine/Process/LinuxOsxProcess.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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();
Expand Down
Loading