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" />
+