Skip to content

[API Proposal]: One-liner for running process with standard handles redirected to null device #128453

@adamsitnik

Description

@adamsitnik

Background and motivation

We have recently introduced two static Process methods: Run and RunAndCaptureText. Both start the process, wait for exit and return exit status. Run inherits standard handles from the parent, while RunAndCaptureText redirects to pipes and captures both std out and err by reding from these pipes. A very common .NET anti-pattern (example: https://github.com/microsoft/aspire/pull/17079/changes#r3243682797) is to redirect both std out and err, start the process and wait for its exit without capturing the output and error just to ensure that nothing gets printed into terminal. This can lead into deadlocks (output and error are not being drained) like it did in dotnet/aspnetcore#65518.

With the new APIs, the users can open the null file handle, configure it via Standard[Input/Output/Error]Handle properties and just pass such ProcessStartInfo to run:

using SafeFileHandle nullHandle = File.OpenNullHandle();
ProcessStartInfo psi = new("exeName", ["--args"])
{
    StandardInputHandle = nullHandle,
    StandardOutputHandle = nullHandle,
    StandardErrorHandle= nullHandle
};

Process.Run(psi);

Since this anti-pattern is so common, we could introduce a helper method that redirects standard handles to null file and waits for the process to exit. One possible name isProcess.RunWithoutOutput, another could be Process.RunAndDiscardOutput.

API Proposal

namespace System.Diagnostics
{
    public class Process : System.ComponentModel.Component, System.IDisposable
    {
        public static ProcessExitStatus RunAndDiscardOutput(ProcessStartInfo startInfo, TimeSpan? timeout = default);
        public static ProcessExitStatus RunAndDiscardOutput(string fileName, IList<string>? arguments = null, TimeSpan? timeout = default);
        public static Task<ProcessExitStatus> RunAndDiscardOutputAsync(ProcessStartInfo startInfo, CancellationToken cancellationToken = default);
        public static Task<ProcessExitStatus> RunAndDiscardOutputAsync(string fileName, IList<string>? arguments = null, CancellationToken cancellationToken = default);
    }
}

API Usage

ProcessExitStatus exitStatus = Process.RunAndDiscardOutput("dotnet", ["restore"]);
if (exitStatus.ExitCode != 0)
{
    // Handle error
}

Alternative Designs

An alternative would be to extend the Run method with a boolean parameter called silent or similar.

namespace System.Diagnostics
{
    public class Process : System.ComponentModel.Component, System.IDisposable
    {
-       public static ProcessExitStatus Run(ProcessStartInfo startInfo, TimeSpan? timeout = default);
+       public static ProcessExitStatus Run(ProcessStartInfo startInfo, bool silent = false, TimeSpan? timeout = default);
-       public static ProcessExitStatus Run(string fileName, IList<string>? arguments = null, TimeSpan? timeout = default);
+       public static ProcessExitStatus Run(string fileName, IList<string>? arguments = null, bool silent = false, TimeSpan? timeout = default);
-       public static Task<ProcessExitStatus> RunAsync(ProcessStartInfo startInfo, CancellationToken cancellationToken = default);
+       public static Task<ProcessExitStatus> RunAsync(ProcessStartInfo startInfo, bool silent = false, CancellationToken cancellationToken = default);
-       public static Task<ProcessExitStatus> RunAsync(string fileName, IList<string>? arguments = null, CancellationToken cancellationToken = default);
+       public static Task<ProcessExitStatus> RunAsync(string fileName, IList<string>? arguments = null, bool silent = false, CancellationToken cancellationToken = default);
    }
}

When set to true, it would redirect all standard handles to null.

Risks

No response

Metadata

Metadata

Assignees

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions