diff --git a/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs b/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs
index 3904ae89bd0b15..35d5da64706695 100644
--- a/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs
+++ b/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs
@@ -253,6 +253,10 @@ public ProcessStartInfo(string fileName, System.Collections.Generic.IEnumerable<
public bool RedirectStandardInput { get { throw null; } set { } }
public bool RedirectStandardOutput { get { throw null; } set { } }
public System.Text.Encoding? StandardErrorEncoding { get { throw null; } set { } }
+ public Microsoft.Win32.SafeHandles.SafeFileHandle? StandardError { get { throw null; } set { } }
+ public Microsoft.Win32.SafeHandles.SafeFileHandle? StandardInput { get { throw null; } set { } }
+ public Microsoft.Win32.SafeHandles.SafeFileHandle? StandardOutput { get { throw null; } set { } }
+ public bool LeaveHandlesOpen { get { throw null; } set { } }
public System.Text.Encoding? StandardInputEncoding { get { throw null; } set { } }
public System.Text.Encoding? StandardOutputEncoding { get { throw null; } set { } }
[System.Diagnostics.CodeAnalysis.AllowNullAttribute]
diff --git a/src/libraries/System.Diagnostics.Process/src/Resources/Strings.resx b/src/libraries/System.Diagnostics.Process/src/Resources/Strings.resx
index 24e76b6da21e1e..b135bffb6d73cf 100644
--- a/src/libraries/System.Diagnostics.Process/src/Resources/Strings.resx
+++ b/src/libraries/System.Diagnostics.Process/src/Resources/Strings.resx
@@ -210,6 +210,9 @@
The Process object must have the UseShellExecute property set to false in order to redirect IO streams.
+
+ The StandardInput, StandardOutput, and StandardError handle properties cannot be used together with RedirectStandardInput, RedirectStandardOutput, and RedirectStandardError.
+
The FileName property should not be a directory unless UseShellExecute is set.
diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs
index 6a068b7468608e..4d9ae60dd2f836 100644
--- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs
+++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs
@@ -1284,6 +1284,27 @@ public bool Start()
throw new InvalidOperationException(SR.CantRedirectStreams);
}
+ bool anyHandle = startInfo.StandardInput is not null || startInfo.StandardOutput is not null || startInfo.StandardError is not null;
+ if (anyHandle)
+ {
+ if (startInfo.UseShellExecute)
+ {
+ throw new InvalidOperationException(SR.CantRedirectStreams);
+ }
+ if (startInfo.StandardInput is not null && startInfo.RedirectStandardInput)
+ {
+ throw new InvalidOperationException(SR.CantSetHandleAndRedirect);
+ }
+ if (startInfo.StandardOutput is not null && startInfo.RedirectStandardOutput)
+ {
+ throw new InvalidOperationException(SR.CantSetHandleAndRedirect);
+ }
+ if (startInfo.StandardError is not null && startInfo.RedirectStandardError)
+ {
+ throw new InvalidOperationException(SR.CantSetHandleAndRedirect);
+ }
+ }
+
//Cannot start a new process and store its handle if the object has been disposed, since finalization has been suppressed.
CheckDisposed();
@@ -1316,17 +1337,29 @@ public bool Start()
try
{
- if (startInfo.RedirectStandardInput)
+ if (startInfo.StandardInput is not null)
+ {
+ childInputPipeHandle = startInfo.StandardInput;
+ }
+ else if (startInfo.RedirectStandardInput)
{
SafeFileHandle.CreateAnonymousPipe(out childInputPipeHandle, out parentInputPipeHandle);
}
- if (startInfo.RedirectStandardOutput)
+ if (startInfo.StandardOutput is not null)
+ {
+ childOutputPipeHandle = startInfo.StandardOutput;
+ }
+ else if (startInfo.RedirectStandardOutput)
{
SafeFileHandle.CreateAnonymousPipe(out parentOutputPipeHandle, out childOutputPipeHandle, asyncRead: OperatingSystem.IsWindows());
}
- if (startInfo.RedirectStandardError)
+ if (startInfo.StandardError is not null)
+ {
+ childErrorPipeHandle = startInfo.StandardError;
+ }
+ else if (startInfo.RedirectStandardError)
{
SafeFileHandle.CreateAnonymousPipe(out parentErrorPipeHandle, out childErrorPipeHandle, asyncRead: OperatingSystem.IsWindows());
}
@@ -1357,9 +1390,22 @@ public bool Start()
{
// We MUST close the child handles, otherwise the parent
// process will not receive EOF when the child process closes its handles.
- childInputPipeHandle?.Dispose();
- childOutputPipeHandle?.Dispose();
- childErrorPipeHandle?.Dispose();
+ // It's OK to do it for handles returned by Console.OpenStandard*Handle APIs,
+ // because these handles are not owned and won't be closed by Dispose.
+ // When LeaveHandlesOpen is true, we must NOT close handles that were passed in
+ // by the caller via StartInfo.StandardInput/Output/Error.
+ if (startInfo.StandardInput is null || !startInfo.LeaveHandlesOpen)
+ {
+ childInputPipeHandle?.Dispose();
+ }
+ if (startInfo.StandardOutput is null || !startInfo.LeaveHandlesOpen)
+ {
+ childOutputPipeHandle?.Dispose();
+ }
+ if (startInfo.StandardError is null || !startInfo.LeaveHandlesOpen)
+ {
+ childErrorPipeHandle?.Dispose();
+ }
}
if (startInfo.RedirectStandardInput)
diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.cs
index 0f4fc0a0cb72c1..15c1c1f3299198 100644
--- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.cs
+++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.cs
@@ -8,6 +8,7 @@
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Text;
+using Microsoft.Win32.SafeHandles;
namespace System.Diagnostics
{
@@ -117,6 +118,89 @@ public string Arguments
public bool RedirectStandardOutput { get; set; }
public bool RedirectStandardError { get; set; }
+ ///
+ /// Gets or sets a that will be used as the standard input of the child process.
+ /// When set, the handle is passed directly to the child process and must be .
+ ///
+ ///
+ ///
+ /// The handle does not need to be inheritable; the runtime will make it inheritable as needed.
+ /// Use to create a pair of connected pipe handles,
+ /// to open a file handle,
+ /// to discard input,
+ /// or to inherit the parent's standard input.
+ ///
+ ///
+ /// By default, will close this handle after starting the child process.
+ /// Set to to keep the handle open.
+ ///
+ ///
+ /// This property cannot be used together with
+ /// and requires to be .
+ ///
+ ///
+ /// A to use as the standard input handle of the child process, or to use the default behavior.
+ public SafeFileHandle? StandardInput { get; set; }
+
+ ///
+ /// Gets or sets a that will be used as the standard output of the child process.
+ /// When set, the handle is passed directly to the child process and must be .
+ ///
+ ///
+ ///
+ /// The handle does not need to be inheritable; the runtime will make it inheritable as needed.
+ /// Use to create a pair of connected pipe handles,
+ /// to open a file handle,
+ /// to discard output,
+ /// or to inherit the parent's standard output.
+ ///
+ ///
+ /// By default, will close this handle after starting the child process.
+ /// Set to to keep the handle open.
+ ///
+ ///
+ /// This property cannot be used together with
+ /// and requires to be .
+ ///
+ ///
+ /// A to use as the standard output handle of the child process, or to use the default behavior.
+ public SafeFileHandle? StandardOutput { get; set; }
+
+ ///
+ /// Gets or sets a that will be used as the standard error of the child process.
+ /// When set, the handle is passed directly to the child process and must be .
+ ///
+ ///
+ ///
+ /// The handle does not need to be inheritable; the runtime will make it inheritable as needed.
+ /// Use to create a pair of connected pipe handles,
+ /// to open a file handle,
+ /// to discard error output,
+ /// or to inherit the parent's standard error.
+ ///
+ ///
+ /// By default, will close this handle after starting the child process.
+ /// Set to to keep the handle open.
+ ///
+ ///
+ /// This property cannot be used together with
+ /// and requires to be .
+ ///
+ ///
+ /// A to use as the standard error handle of the child process, or to use the default behavior.
+ public SafeFileHandle? StandardError { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether the , ,
+ /// and handles should be left open after the process is started.
+ ///
+ ///
+ /// When (the default), the handles are closed by
+ /// after starting the child process. When , the caller is responsible for closing the handles.
+ ///
+ /// to leave the handles open; to close them after the process starts. The default is .
+ public bool LeaveHandlesOpen { get; set; }
+
public Encoding? StandardInputEncoding { get; set; }
public Encoding? StandardErrorEncoding { get; set; }
diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.cs
new file mode 100644
index 00000000000000..996fb332465b02
--- /dev/null
+++ b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.cs
@@ -0,0 +1,298 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.IO;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.Win32.SafeHandles;
+using Xunit;
+
+namespace System.Diagnostics.Tests
+{
+ public class ProcessHandlesTests : ProcessTestBase
+ {
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public async Task CanRedirectOutputToPipe(bool readAsync)
+ {
+ ProcessStartInfo startInfo = OperatingSystem.IsWindows()
+ ? new("cmd") { ArgumentList = { "/c", "echo Test" } }
+ : new("sh") { ArgumentList = { "-c", "echo 'Test'" } };
+
+ SafeFileHandle.CreateAnonymousPipe(out SafeFileHandle readPipe, out SafeFileHandle writePipe, asyncRead: readAsync);
+
+ startInfo.StandardOutput = writePipe;
+
+ using (readPipe)
+ using (writePipe)
+ {
+ using Process process = Process.Start(startInfo)!;
+
+ using FileStream fileStream = new(readPipe, FileAccess.Read, bufferSize: 1, isAsync: readAsync);
+ using StreamReader streamReader = new(fileStream);
+
+ string output = readAsync
+ ? await streamReader.ReadToEndAsync()
+ : streamReader.ReadToEnd();
+
+ Assert.Equal(OperatingSystem.IsWindows() ? "Test\r\n" : "Test\n", output);
+
+ process.WaitForExit();
+ Assert.Equal(0, process.ExitCode);
+ }
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public async Task CanRedirectOutputAndErrorToDifferentPipes(bool readAsync)
+ {
+ ProcessStartInfo startInfo = OperatingSystem.IsWindows()
+ ? new("cmd") { ArgumentList = { "/c", "echo Hello from stdout && echo Error from stderr 1>&2" } }
+ : new("sh") { ArgumentList = { "-c", "echo 'Hello from stdout' && echo 'Error from stderr' >&2" } };
+
+ SafeFileHandle.CreateAnonymousPipe(out SafeFileHandle outputRead, out SafeFileHandle outputWrite, asyncRead: readAsync);
+ SafeFileHandle.CreateAnonymousPipe(out SafeFileHandle errorRead, out SafeFileHandle errorWrite, asyncRead: readAsync);
+
+ startInfo.StandardOutput = outputWrite;
+ startInfo.StandardError = errorWrite;
+
+ using (outputRead)
+ using (outputWrite)
+ using (errorRead)
+ using (errorWrite)
+ {
+ using Process process = Process.Start(startInfo)!;
+
+ using FileStream outputStream = new(outputRead, FileAccess.Read, bufferSize: 1, isAsync: readAsync);
+ using FileStream errorStream = new(errorRead, FileAccess.Read, bufferSize: 1, isAsync: readAsync);
+ using StreamReader outputReader = new(outputStream);
+ using StreamReader errorReader = new(errorStream);
+
+ Task outputTask = outputReader.ReadToEndAsync();
+ Task errorTask = errorReader.ReadToEndAsync();
+
+ Assert.Equal(OperatingSystem.IsWindows() ? "Hello from stdout \r\n" : "Hello from stdout\n", await outputTask);
+ Assert.Equal(OperatingSystem.IsWindows() ? "Error from stderr \r\n" : "Error from stderr\n", await errorTask);
+
+ process.WaitForExit();
+ Assert.Equal(0, process.ExitCode);
+ }
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public async Task CanRedirectToInheritedHandles(bool useAsync)
+ {
+ ProcessStartInfo startInfo = OperatingSystem.IsWindows()
+ ? new("cmd") { ArgumentList = { "/c", "exit 42" } }
+ : new("sh") { ArgumentList = { "-c", "exit 42" } };
+
+ startInfo.StandardInput = Console.OpenStandardInputHandle();
+ startInfo.StandardOutput = Console.OpenStandardOutputHandle();
+ startInfo.StandardError = Console.OpenStandardErrorHandle();
+
+ using Process process = Process.Start(startInfo)!;
+
+ if (useAsync)
+ {
+ await process.WaitForExitAsync();
+ }
+ else
+ {
+ process.WaitForExit();
+ }
+
+ Assert.Equal(42, process.ExitCode);
+ }
+
+ [Fact]
+ public async Task CanImplementPiping()
+ {
+ SafeFileHandle readPipe = null!;
+ SafeFileHandle writePipe = null!;
+ string? tempFile = null;
+
+ try
+ {
+ SafeFileHandle.CreateAnonymousPipe(out readPipe, out writePipe);
+ tempFile = Path.GetTempFileName();
+
+ ProcessStartInfo producerInfo;
+ ProcessStartInfo consumerInfo;
+ string expectedOutput;
+
+ if (OperatingSystem.IsWindows())
+ {
+ producerInfo = new("cmd")
+ {
+ ArgumentList = { "/c", "echo hello world & echo test line & echo another test" }
+ };
+ consumerInfo = new("findstr")
+ {
+ ArgumentList = { "test" }
+ };
+ // findstr adds a trailing space on Windows
+ expectedOutput = "test line \nanother test\n";
+ }
+ else
+ {
+ producerInfo = new("sh")
+ {
+ ArgumentList = { "-c", "printf 'hello world\\ntest line\\nanother test\\n'" }
+ };
+ consumerInfo = new("grep")
+ {
+ ArgumentList = { "test" }
+ };
+ expectedOutput = "test line\nanother test\n";
+ }
+
+ producerInfo.StandardOutput = writePipe;
+
+ using SafeFileHandle outputHandle = File.OpenHandle(tempFile, FileMode.Create, FileAccess.Write, FileShare.ReadWrite);
+
+ consumerInfo.StandardInput = readPipe;
+ consumerInfo.StandardOutput = outputHandle;
+
+ using Process producer = Process.Start(producerInfo)!;
+ using Process consumer = Process.Start(consumerInfo)!;
+
+ producer.WaitForExit();
+ consumer.WaitForExit();
+
+ string result = File.ReadAllText(tempFile);
+ Assert.Equal(expectedOutput, result, ignoreLineEndingDifferences: true);
+ }
+ finally
+ {
+ readPipe.Dispose();
+ writePipe.Dispose();
+
+ if (tempFile is not null && File.Exists(tempFile))
+ {
+ File.Delete(tempFile);
+ }
+ }
+ }
+
+ [Fact]
+ public void LeaveHandlesOpen_KeepsHandleOpen()
+ {
+ string tempFile = Path.GetTempFileName();
+
+ try
+ {
+ ProcessStartInfo startInfo = new("hostname");
+
+ SafeFileHandle outputHandle = File.OpenHandle(tempFile, FileMode.Create, FileAccess.Write, FileShare.ReadWrite);
+
+ startInfo.StandardOutput = outputHandle;
+ startInfo.LeaveHandlesOpen = true;
+
+ using Process process = Process.Start(startInfo)!;
+ process.WaitForExit();
+
+ Assert.Equal(0, process.ExitCode);
+ Assert.False(outputHandle.IsClosed);
+
+ outputHandle.Dispose();
+ }
+ finally
+ {
+ if (File.Exists(tempFile))
+ {
+ File.Delete(tempFile);
+ }
+ }
+ }
+
+ [Fact]
+ public void StandardInput_WithRedirectStandardInput_Throws()
+ {
+ ProcessStartInfo startInfo = new("cmd")
+ {
+ RedirectStandardInput = true,
+ StandardInput = Console.OpenStandardInputHandle()
+ };
+
+ using (startInfo.StandardInput)
+ {
+ Assert.Throws(() => Process.Start(startInfo));
+ }
+ }
+
+ [Fact]
+ public void StandardOutput_WithRedirectStandardOutput_Throws()
+ {
+ ProcessStartInfo startInfo = new("cmd")
+ {
+ RedirectStandardOutput = true,
+ StandardOutput = Console.OpenStandardOutputHandle()
+ };
+
+ using (startInfo.StandardOutput)
+ {
+ Assert.Throws(() => Process.Start(startInfo));
+ }
+ }
+
+ [Fact]
+ public void StandardError_WithRedirectStandardError_Throws()
+ {
+ ProcessStartInfo startInfo = new("cmd")
+ {
+ RedirectStandardError = true,
+ StandardError = Console.OpenStandardErrorHandle()
+ };
+
+ using (startInfo.StandardError)
+ {
+ Assert.Throws(() => Process.Start(startInfo));
+ }
+ }
+
+ [Fact]
+ public void StandardHandles_WithUseShellExecute_Throws()
+ {
+ ProcessStartInfo startInfo = new("cmd")
+ {
+ UseShellExecute = true,
+ StandardOutput = Console.OpenStandardOutputHandle()
+ };
+
+ using (startInfo.StandardOutput)
+ {
+ Assert.Throws(() => Process.Start(startInfo));
+ }
+ }
+
+ [Fact]
+ public void StandardHandles_DefaultIsNull()
+ {
+ ProcessStartInfo startInfo = new("cmd");
+ Assert.Null(startInfo.StandardInput);
+ Assert.Null(startInfo.StandardOutput);
+ Assert.Null(startInfo.StandardError);
+ }
+
+ [Fact]
+ public void StandardHandles_CanSetAndGet()
+ {
+ using SafeFileHandle handle = Console.OpenStandardOutputHandle();
+
+ ProcessStartInfo startInfo = new("cmd")
+ {
+ StandardInput = handle,
+ StandardOutput = handle,
+ StandardError = handle
+ };
+
+ Assert.Same(handle, startInfo.StandardInput);
+ Assert.Same(handle, startInfo.StandardOutput);
+ Assert.Same(handle, startInfo.StandardError);
+ }
+ }
+}
diff --git a/src/libraries/System.Diagnostics.Process/tests/System.Diagnostics.Process.Tests.csproj b/src/libraries/System.Diagnostics.Process/tests/System.Diagnostics.Process.Tests.csproj
index e44431ae36405a..67cd6d66a06ece 100644
--- a/src/libraries/System.Diagnostics.Process/tests/System.Diagnostics.Process.Tests.csproj
+++ b/src/libraries/System.Diagnostics.Process/tests/System.Diagnostics.Process.Tests.csproj
@@ -26,6 +26,7 @@
+