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 1294bc9accf1c0..33a1a8519089b1 100644 --- a/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs +++ b/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs @@ -55,6 +55,7 @@ public MonitoringDescriptionAttribute(string description) { } public partial class Process : System.ComponentModel.Component, System.IDisposable { public Process() { } + public Process(Microsoft.Win32.SafeHandles.SafeProcessHandle processHandle, Microsoft.Win32.SafeHandles.SafeFileHandle? standardInput = null, Microsoft.Win32.SafeHandles.SafeFileHandle? standardOutput = null, Microsoft.Win32.SafeHandles.SafeFileHandle? standardError = null, System.Diagnostics.ProcessStartInfo? startInfo = null) { } public int BasePriority { get { throw null; } } public bool EnableRaisingEvents { get { throw null; } set { } } public int ExitCode { get { throw null; } } diff --git a/src/libraries/System.Diagnostics.Process/src/Resources/Strings.resx b/src/libraries/System.Diagnostics.Process/src/Resources/Strings.resx index 92a6dd8e10675e..255e02d15303d3 100644 --- a/src/libraries/System.Diagnostics.Process/src/Resources/Strings.resx +++ b/src/libraries/System.Diagnostics.Process/src/Resources/Strings.resx @@ -306,6 +306,9 @@ The output byte buffer is too small to contain the encoded data, encoding '{0}' fallback '{1}'. + + The handle must be opened for asynchronous I/O on Windows. + The output char buffer is too small to contain the decoded characters, encoding '{0}' fallback '{1}'. 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 8966e578389e92..6d05d243111409 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs @@ -107,6 +107,58 @@ public Process() _errorStreamReadMode = StreamReadMode.Undefined; } + /// + /// Initializes a new instance of the class from an existing process handle, + /// with optional standard I/O stream handles and start info. + /// + /// A representing the process. + /// An optional for the standard input stream of the process. The handle must support write access. + /// An optional for the standard output stream of the process. The handle must support read access. + /// An optional for the standard error stream of the process. The handle must support read access. + /// An optional containing encoding information for the streams. + public Process(SafeProcessHandle processHandle, SafeFileHandle? standardInput = null, SafeFileHandle? standardOutput = null, SafeFileHandle? standardError = null, ProcessStartInfo? startInfo = null) + { + ArgumentNullException.ThrowIfNull(processHandle); + + GC.SuppressFinalize(this); + _machineName = "."; + _outputStreamReadMode = StreamReadMode.Undefined; + _errorStreamReadMode = StreamReadMode.Undefined; + _startInfo = startInfo; + + SetProcessHandle(processHandle); + SetProcessId(processHandle.ProcessId); + + if (standardInput is not null) + { + _standardInput = new StreamWriter(OpenStream(standardInput, FileAccess.Write), + startInfo?.StandardInputEncoding ?? GetStandardInputEncoding(), StreamBufferSize) + { + AutoFlush = true + }; + } + if (standardOutput is not null) + { + if (OperatingSystem.IsWindows() && !standardOutput.IsAsync) + { + throw new ArgumentException(SR.Argument_HandleNotAsync, nameof(standardOutput)); + } + + _standardOutput = new StreamReader(OpenStream(standardOutput, FileAccess.Read), + startInfo?.StandardOutputEncoding ?? GetStandardOutputEncoding(), true, StreamBufferSize); + } + if (standardError is not null) + { + if (OperatingSystem.IsWindows() && !standardError.IsAsync) + { + throw new ArgumentException(SR.Argument_HandleNotAsync, nameof(standardError)); + } + + _standardError = new StreamReader(OpenStream(standardError, FileAccess.Read), + startInfo?.StandardErrorEncoding ?? GetStandardOutputEncoding(), true, StreamBufferSize); + } + } + private Process(string machineName, bool isRemoteMachine, int processId, ProcessInfo? processInfo) { GC.SuppressFinalize(this); diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs index c49b71627e0a0e..30121fe862b145 100644 --- a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs @@ -1,13 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Collections.Generic; using System.ComponentModel; -using System.IO; -using System.IO.Pipes; -using System.Threading.Tasks; +using System.Runtime.InteropServices; using Microsoft.DotNet.RemoteExecutor; -using Microsoft.DotNet.XUnitExtensions; using Microsoft.Win32.SafeHandles; using Xunit; @@ -15,6 +11,8 @@ namespace System.Diagnostics.Tests { public partial class ProcessHandlesTests { + private const nint INVALID_HANDLE_VALUE = -1; + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] public void ProcessStartedWithInvalidHandles_ConsoleReportsInvalidHandles() { @@ -27,7 +25,7 @@ public void ProcessStartedWithInvalidHandles_ConsoleReportsInvalidHandles() return RemoteExecutor.SuccessExitCode; }); - Assert.Equal(RemoteExecutor.SuccessExitCode, RunWithInvalidHandles(process.StartInfo)); + Assert.Equal(RemoteExecutor.SuccessExitCode, RunWithHandles(process.StartInfo, INVALID_HANDLE_VALUE, INVALID_HANDLE_VALUE, INVALID_HANDLE_VALUE)); } [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] @@ -64,7 +62,7 @@ public void ProcessStartedWithInvalidHandles_CanStartChildProcessWithDerivedInva } }, restrictHandles.ToString(), killOnParentExit.ToString()); - Assert.Equal(RemoteExecutor.SuccessExitCode, RunWithInvalidHandles(process.StartInfo)); + Assert.Equal(RemoteExecutor.SuccessExitCode, RunWithHandles(process.StartInfo, INVALID_HANDLE_VALUE, INVALID_HANDLE_VALUE, INVALID_HANDLE_VALUE)); } [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] @@ -102,13 +100,104 @@ public void ProcessStartedWithInvalidHandles_CanRedirectOutput(bool restrictHand } }, restrictHandles.ToString()); - Assert.Equal(RemoteExecutor.SuccessExitCode, RunWithInvalidHandles(process.StartInfo)); + Assert.Equal(RemoteExecutor.SuccessExitCode, RunWithHandles(process.StartInfo, INVALID_HANDLE_VALUE, INVALID_HANDLE_VALUE, INVALID_HANDLE_VALUE)); + } + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public void ProcessStartedWithAnonymousPipeHandles_CanCaptureOutput() + { + SafeFileHandle.CreateAnonymousPipe(out SafeFileHandle outputReadHandle, out SafeFileHandle outputWriteHandle, asyncRead: true); + SafeFileHandle.CreateAnonymousPipe(out SafeFileHandle errorReadHandle, out SafeFileHandle errorWriteHandle, asyncRead: true); + + using (outputReadHandle) + using (outputWriteHandle) + using (errorReadHandle) + using (errorWriteHandle) + { + // Enable inheritance on the write ends so the child process can use them. + if (!Interop.Kernel32.SetHandleInformation( + outputWriteHandle, + Interop.Kernel32.HandleFlags.HANDLE_FLAG_INHERIT, + Interop.Kernel32.HandleFlags.HANDLE_FLAG_INHERIT)) + { + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + + if (!Interop.Kernel32.SetHandleInformation( + errorWriteHandle, + Interop.Kernel32.HandleFlags.HANDLE_FLAG_INHERIT, + Interop.Kernel32.HandleFlags.HANDLE_FLAG_INHERIT)) + { + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + + using Process remoteProcess = CreateProcess(() => + { + Console.Write("stdout_hello"); + Console.Error.Write("stderr_hello"); + + return RemoteExecutor.SuccessExitCode; + }); + + Interop.Kernel32.PROCESS_INFORMATION processInfo = CreateProcessWithHandles( + remoteProcess.StartInfo, + INVALID_HANDLE_VALUE, + outputWriteHandle.DangerousGetHandle(), + errorWriteHandle.DangerousGetHandle(), + inheritHandles: true); + + try + { + SafeProcessHandle safeProcessHandle = new(processInfo.hProcess, ownsHandle: true); + + using Process process = new( + safeProcessHandle, + standardOutput: outputReadHandle, + standardError: errorReadHandle); + + // Close the write ends so reads don't block once the child exits. + outputWriteHandle.Close(); + errorWriteHandle.Close(); + + (string stdout, string stderr) = process.ReadAllText(); + + Assert.Equal("stdout_hello", stdout); + Assert.Equal("stderr_hello", stderr); + Assert.Equal(RemoteExecutor.SuccessExitCode, process.ExitCode); + } + finally + { + Interop.Kernel32.CloseHandle(processInfo.hThread); + } + } } - private unsafe int RunWithInvalidHandles(ProcessStartInfo startInfo) + private int RunWithHandles(ProcessStartInfo startInfo, nint hStdInput, nint hStdOutput, nint hStdError, bool inheritHandles = false) { - const nint INVALID_HANDLE_VALUE = -1; + Interop.Kernel32.PROCESS_INFORMATION processInfo = CreateProcessWithHandles(startInfo, hStdInput, hStdOutput, hStdError, inheritHandles); + + try + { + using SafeProcessHandle safeProcessHandle = new(processInfo.hProcess, ownsHandle: true); + + try + { + ProcessExitStatus exitStatus = safeProcessHandle.WaitForExitOrKillOnTimeout(TimeSpan.FromMilliseconds(WaitInMS)); + return exitStatus.ExitCode; + } + finally + { + safeProcessHandle.Kill(); + } + } + finally + { + Interop.Kernel32.CloseHandle(processInfo.hThread); + } + } + private static unsafe Interop.Kernel32.PROCESS_INFORMATION CreateProcessWithHandles(ProcessStartInfo startInfo, nint hStdInput, nint hStdOutput, nint hStdError, bool inheritHandles) + { // RemoteExector has provided us with the right path and arguments, // we just need to add the terminating null character. string arguments = $"\"{startInfo.FileName}\" {startInfo.Arguments}\0"; @@ -118,22 +207,21 @@ private unsafe int RunWithInvalidHandles(ProcessStartInfo startInfo) Interop.Kernel32.SECURITY_ATTRIBUTES unused_SecAttrs = default; startupInfoEx.StartupInfo.cb = sizeof(Interop.Kernel32.STARTUPINFOEX); - startupInfoEx.StartupInfo.hStdInput = INVALID_HANDLE_VALUE; - startupInfoEx.StartupInfo.hStdOutput = INVALID_HANDLE_VALUE; - startupInfoEx.StartupInfo.hStdError = INVALID_HANDLE_VALUE; + startupInfoEx.StartupInfo.hStdInput = hStdInput; + startupInfoEx.StartupInfo.hStdOutput = hStdOutput; + startupInfoEx.StartupInfo.hStdError = hStdError; // If STARTF_USESTDHANDLES is not set, the new process will inherit the standard handles. startupInfoEx.StartupInfo.dwFlags = Interop.Advapi32.StartupInfoOptions.STARTF_USESTDHANDLES; - bool retVal = false; fixed (char* commandLinePtr = arguments) { - retVal = Interop.Kernel32.CreateProcess( + bool retVal = Interop.Kernel32.CreateProcess( null, commandLinePtr, ref unused_SecAttrs, ref unused_SecAttrs, - bInheritHandles: false, + bInheritHandles: inheritHandles, Interop.Kernel32.EXTENDED_STARTUPINFO_PRESENT, null, null, @@ -147,24 +235,7 @@ private unsafe int RunWithInvalidHandles(ProcessStartInfo startInfo) } } - try - { - using SafeProcessHandle safeProcessHandle = new(processInfo.hProcess, ownsHandle: true); - - try - { - ProcessExitStatus exitStatus = safeProcessHandle.WaitForExitOrKillOnTimeout(TimeSpan.FromMilliseconds(WaitInMS)); - return exitStatus.ExitCode; - } - finally - { - safeProcessHandle.Kill(); - } - } - finally - { - Interop.Kernel32.CloseHandle(processInfo.hThread); - } + return processInfo; } private static unsafe string GetSafeFileHandleId(SafeFileHandle handle) 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 f1ed6055933db3..546ebe34db4467 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 @@ -75,6 +75,8 @@ Link="Common\Interop\Windows\Kernel32\Interop.SetConsoleCtrlHandler.cs" /> +