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 @@ +