From 3a6fc1151df4ffea9ed7f3ff7396d5302b6cddc7 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 22 May 2026 15:22:48 +0000
Subject: [PATCH 1/4] Add Process ctor accepting SafeProcessHandle and optional
standard stream handles
Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/3a2f55b9-2cc6-46ba-87af-1dc53029c488
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
---
.../ref/System.Diagnostics.Process.cs | 1 +
.../src/System/Diagnostics/Process.cs | 46 +++++++
.../tests/ProcessHandlesTests.Windows.cs | 119 ++++++++++++++++--
.../System.Diagnostics.Process.Tests.csproj | 2 +
4 files changed, 156 insertions(+), 12 deletions(-)
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/System/Diagnostics/Process.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs
index 8966e578389e92..f2e1fd4409b644 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,52 @@ 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.
+ /// An optional for the standard output stream of the process.
+ /// An optional for the standard error stream of the process.
+ /// 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;
+
+ SetProcessHandle(processHandle);
+ SetProcessId(processHandle.ProcessId);
+
+ if (startInfo is not null)
+ {
+ _startInfo = startInfo;
+ }
+
+ if (standardInput is not null)
+ {
+ _standardInput = new StreamWriter(OpenStream(standardInput, FileAccess.Write),
+ startInfo?.StandardInputEncoding ?? GetStandardInputEncoding(), StreamBufferSize)
+ {
+ AutoFlush = true
+ };
+ }
+ if (standardOutput is not null)
+ {
+ _standardOutput = new StreamReader(OpenStream(standardOutput, FileAccess.Read),
+ startInfo?.StandardOutputEncoding ?? GetStandardOutputEncoding(), true, StreamBufferSize);
+ }
+ if (standardError is not null)
+ {
+ _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..00fa9644626e53 100644
--- a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs
+++ b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs
@@ -1,13 +1,11 @@
// 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.Runtime.InteropServices;
using System.Threading.Tasks;
using Microsoft.DotNet.RemoteExecutor;
-using Microsoft.DotNet.XUnitExtensions;
using Microsoft.Win32.SafeHandles;
using Xunit;
@@ -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, (nint)(-1), (nint)(-1), (nint)(-1)));
}
[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, (nint)(-1), (nint)(-1), (nint)(-1)));
}
[ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
@@ -102,13 +100,110 @@ public void ProcessStartedWithInvalidHandles_CanRedirectOutput(bool restrictHand
}
}, restrictHandles.ToString());
- Assert.Equal(RemoteExecutor.SuccessExitCode, RunWithInvalidHandles(process.StartInfo));
+ Assert.Equal(RemoteExecutor.SuccessExitCode, RunWithHandles(process.StartInfo, (nint)(-1), (nint)(-1), (nint)(-1)));
}
- private unsafe int RunWithInvalidHandles(ProcessStartInfo startInfo)
+ [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
+ public unsafe void ProcessStartedWithAnonymousPipeHandles_CanCaptureOutput()
{
- const nint INVALID_HANDLE_VALUE = -1;
+ SafeFileHandle.CreateAnonymousPipe(out SafeFileHandle outputReadHandle, out SafeFileHandle outputWriteHandle);
+ SafeFileHandle.CreateAnonymousPipe(out SafeFileHandle errorReadHandle, out SafeFileHandle errorWriteHandle);
+
+ 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;
+ });
+
+ ProcessStartInfo startInfo = remoteProcess.StartInfo;
+ string arguments = $"\"{startInfo.FileName}\" {startInfo.Arguments}\0";
+
+ Interop.Kernel32.STARTUPINFOEX startupInfoEx = default;
+ Interop.Kernel32.PROCESS_INFORMATION processInfo = default;
+ Interop.Kernel32.SECURITY_ATTRIBUTES unused_SecAttrs = default;
+
+ startupInfoEx.StartupInfo.cb = sizeof(Interop.Kernel32.STARTUPINFOEX);
+ startupInfoEx.StartupInfo.hStdInput = (nint)(-1);
+ startupInfoEx.StartupInfo.hStdOutput = outputWriteHandle.DangerousGetHandle();
+ startupInfoEx.StartupInfo.hStdError = errorWriteHandle.DangerousGetHandle();
+ startupInfoEx.StartupInfo.dwFlags = Interop.Advapi32.StartupInfoOptions.STARTF_USESTDHANDLES;
+
+ fixed (char* commandLinePtr = arguments)
+ {
+ bool retVal = Interop.Kernel32.CreateProcess(
+ null,
+ commandLinePtr,
+ ref unused_SecAttrs,
+ ref unused_SecAttrs,
+ bInheritHandles: true,
+ Interop.Kernel32.EXTENDED_STARTUPINFO_PRESENT,
+ null,
+ null,
+ &startupInfoEx,
+ &processInfo
+ );
+
+ if (!retVal)
+ {
+ throw new Win32Exception();
+ }
+ }
+
+ 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 = process.StandardOutput.ReadToEnd();
+ string stderr = process.StandardError.ReadToEnd();
+ process.WaitForExit(WaitInMS);
+
+ 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 RunWithHandles(ProcessStartInfo startInfo, nint hStdInput, nint hStdOutput, nint hStdError)
+ {
// 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,9 +213,9 @@ 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;
@@ -133,7 +228,7 @@ private unsafe int RunWithInvalidHandles(ProcessStartInfo startInfo)
commandLinePtr,
ref unused_SecAttrs,
ref unused_SecAttrs,
- bInheritHandles: false,
+ bInheritHandles: true,
Interop.Kernel32.EXTENDED_STARTUPINFO_PRESENT,
null,
null,
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" />
+
From 157b2eebdfc992cf33df6bb96e436cc07ef0e0f9 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 22 May 2026 15:25:47 +0000
Subject: [PATCH 2/4] Address code review: add bInheritHandles param, improve
docs, remove unused usings
Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/3a2f55b9-2cc6-46ba-87af-1dc53029c488
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
---
.../src/System/Diagnostics/Process.cs | 6 +++---
.../tests/ProcessHandlesTests.Windows.cs | 6 ++----
2 files changed, 5 insertions(+), 7 deletions(-)
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 f2e1fd4409b644..9ea1d59bb0f400 100644
--- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs
+++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs
@@ -112,9 +112,9 @@ public Process()
/// with optional standard I/O stream handles and start info.
///
/// A representing the process.
- /// An optional for the standard input stream of the process.
- /// An optional for the standard output stream of the process.
- /// An optional for the standard error stream of 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)
{
diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs
index 00fa9644626e53..b23f687f1269c2 100644
--- a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs
+++ b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs
@@ -2,9 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System.ComponentModel;
-using System.IO;
using System.Runtime.InteropServices;
-using System.Threading.Tasks;
using Microsoft.DotNet.RemoteExecutor;
using Microsoft.Win32.SafeHandles;
using Xunit;
@@ -202,7 +200,7 @@ public unsafe void ProcessStartedWithAnonymousPipeHandles_CanCaptureOutput()
}
}
- private unsafe int RunWithHandles(ProcessStartInfo startInfo, nint hStdInput, nint hStdOutput, nint hStdError)
+ private unsafe int RunWithHandles(ProcessStartInfo startInfo, nint hStdInput, nint hStdOutput, nint hStdError, bool inheritHandles = false)
{
// RemoteExector has provided us with the right path and arguments,
// we just need to add the terminating null character.
@@ -228,7 +226,7 @@ private unsafe int RunWithHandles(ProcessStartInfo startInfo, nint hStdInput, ni
commandLinePtr,
ref unused_SecAttrs,
ref unused_SecAttrs,
- bInheritHandles: true,
+ bInheritHandles: inheritHandles,
Interop.Kernel32.EXTENDED_STARTUPINFO_PRESENT,
null,
null,
From a53a1fcd774c4b87e7f8e8127e5bcd398a98b44f Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 22 May 2026 16:19:30 +0000
Subject: [PATCH 3/4] Address review feedback: simplify startInfo assignment,
use INVALID_HANDLE_VALUE, extract CreateProcessWithHandles helper
Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/12cdb475-8f8c-4309-b5d7-d0b02c675048
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
---
.../src/System/Diagnostics/Process.cs | 6 +-
.../tests/ProcessHandlesTests.Windows.cs | 97 ++++++++-----------
2 files changed, 40 insertions(+), 63 deletions(-)
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 9ea1d59bb0f400..e7e18a7a307e4d 100644
--- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs
+++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs
@@ -124,15 +124,11 @@ public Process(SafeProcessHandle processHandle, SafeFileHandle? standardInput =
_machineName = ".";
_outputStreamReadMode = StreamReadMode.Undefined;
_errorStreamReadMode = StreamReadMode.Undefined;
+ _startInfo = startInfo;
SetProcessHandle(processHandle);
SetProcessId(processHandle.ProcessId);
- if (startInfo is not null)
- {
- _startInfo = startInfo;
- }
-
if (standardInput is not null)
{
_standardInput = new StreamWriter(OpenStream(standardInput, FileAccess.Write),
diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs
index b23f687f1269c2..446af6d509b71c 100644
--- a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs
+++ b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs
@@ -11,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()
{
@@ -23,7 +25,7 @@ public void ProcessStartedWithInvalidHandles_ConsoleReportsInvalidHandles()
return RemoteExecutor.SuccessExitCode;
});
- Assert.Equal(RemoteExecutor.SuccessExitCode, RunWithHandles(process.StartInfo, (nint)(-1), (nint)(-1), (nint)(-1)));
+ Assert.Equal(RemoteExecutor.SuccessExitCode, RunWithHandles(process.StartInfo, INVALID_HANDLE_VALUE, INVALID_HANDLE_VALUE, INVALID_HANDLE_VALUE));
}
[ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
@@ -60,7 +62,7 @@ public void ProcessStartedWithInvalidHandles_CanStartChildProcessWithDerivedInva
}
}, restrictHandles.ToString(), killOnParentExit.ToString());
- Assert.Equal(RemoteExecutor.SuccessExitCode, RunWithHandles(process.StartInfo, (nint)(-1), (nint)(-1), (nint)(-1)));
+ Assert.Equal(RemoteExecutor.SuccessExitCode, RunWithHandles(process.StartInfo, INVALID_HANDLE_VALUE, INVALID_HANDLE_VALUE, INVALID_HANDLE_VALUE));
}
[ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
@@ -98,11 +100,11 @@ public void ProcessStartedWithInvalidHandles_CanRedirectOutput(bool restrictHand
}
}, restrictHandles.ToString());
- Assert.Equal(RemoteExecutor.SuccessExitCode, RunWithHandles(process.StartInfo, (nint)(-1), (nint)(-1), (nint)(-1)));
+ Assert.Equal(RemoteExecutor.SuccessExitCode, RunWithHandles(process.StartInfo, INVALID_HANDLE_VALUE, INVALID_HANDLE_VALUE, INVALID_HANDLE_VALUE));
}
[ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
- public unsafe void ProcessStartedWithAnonymousPipeHandles_CanCaptureOutput()
+ public void ProcessStartedWithAnonymousPipeHandles_CanCaptureOutput()
{
SafeFileHandle.CreateAnonymousPipe(out SafeFileHandle outputReadHandle, out SafeFileHandle outputWriteHandle);
SafeFileHandle.CreateAnonymousPipe(out SafeFileHandle errorReadHandle, out SafeFileHandle errorWriteHandle);
@@ -137,39 +139,12 @@ public unsafe void ProcessStartedWithAnonymousPipeHandles_CanCaptureOutput()
return RemoteExecutor.SuccessExitCode;
});
- ProcessStartInfo startInfo = remoteProcess.StartInfo;
- string arguments = $"\"{startInfo.FileName}\" {startInfo.Arguments}\0";
-
- Interop.Kernel32.STARTUPINFOEX startupInfoEx = default;
- Interop.Kernel32.PROCESS_INFORMATION processInfo = default;
- Interop.Kernel32.SECURITY_ATTRIBUTES unused_SecAttrs = default;
-
- startupInfoEx.StartupInfo.cb = sizeof(Interop.Kernel32.STARTUPINFOEX);
- startupInfoEx.StartupInfo.hStdInput = (nint)(-1);
- startupInfoEx.StartupInfo.hStdOutput = outputWriteHandle.DangerousGetHandle();
- startupInfoEx.StartupInfo.hStdError = errorWriteHandle.DangerousGetHandle();
- startupInfoEx.StartupInfo.dwFlags = Interop.Advapi32.StartupInfoOptions.STARTF_USESTDHANDLES;
-
- fixed (char* commandLinePtr = arguments)
- {
- bool retVal = Interop.Kernel32.CreateProcess(
- null,
- commandLinePtr,
- ref unused_SecAttrs,
- ref unused_SecAttrs,
- bInheritHandles: true,
- Interop.Kernel32.EXTENDED_STARTUPINFO_PRESENT,
- null,
- null,
- &startupInfoEx,
- &processInfo
- );
-
- if (!retVal)
- {
- throw new Win32Exception();
- }
- }
+ Interop.Kernel32.PROCESS_INFORMATION processInfo = CreateProcessWithHandles(
+ remoteProcess.StartInfo,
+ INVALID_HANDLE_VALUE,
+ outputWriteHandle.DangerousGetHandle(),
+ errorWriteHandle.DangerousGetHandle(),
+ inheritHandles: true);
try
{
@@ -200,7 +175,31 @@ public unsafe void ProcessStartedWithAnonymousPipeHandles_CanCaptureOutput()
}
}
- private unsafe int RunWithHandles(ProcessStartInfo startInfo, nint hStdInput, nint hStdOutput, nint hStdError, bool inheritHandles = false)
+ private int RunWithHandles(ProcessStartInfo startInfo, nint hStdInput, nint hStdOutput, nint hStdError, bool inheritHandles = false)
+ {
+ 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.
@@ -218,10 +217,9 @@ private unsafe int RunWithHandles(ProcessStartInfo startInfo, nint hStdInput, ni
// 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,
@@ -240,24 +238,7 @@ private unsafe int RunWithHandles(ProcessStartInfo startInfo, nint hStdInput, ni
}
}
- 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)
From a6488fc5703d9c80c1804d42be7685027da51b33 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 22 May 2026 17:18:45 +0000
Subject: [PATCH 4/4] Address feedback: asyncRead: true, ReadAllText, IsAsync
validation on Windows
Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/b6feaf9e-a3a5-4056-a01a-9aa9761be8be
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
---
.../src/Resources/Strings.resx | 3 +++
.../src/System/Diagnostics/Process.cs | 10 ++++++++++
.../tests/ProcessHandlesTests.Windows.cs | 9 +++------
3 files changed, 16 insertions(+), 6 deletions(-)
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 e7e18a7a307e4d..6d05d243111409 100644
--- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs
+++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs
@@ -139,11 +139,21 @@ public Process(SafeProcessHandle processHandle, SafeFileHandle? standardInput =
}
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);
}
diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs
index 446af6d509b71c..30121fe862b145 100644
--- a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs
+++ b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs
@@ -106,8 +106,8 @@ public void ProcessStartedWithInvalidHandles_CanRedirectOutput(bool restrictHand
[ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
public void ProcessStartedWithAnonymousPipeHandles_CanCaptureOutput()
{
- SafeFileHandle.CreateAnonymousPipe(out SafeFileHandle outputReadHandle, out SafeFileHandle outputWriteHandle);
- SafeFileHandle.CreateAnonymousPipe(out SafeFileHandle errorReadHandle, out SafeFileHandle errorWriteHandle);
+ SafeFileHandle.CreateAnonymousPipe(out SafeFileHandle outputReadHandle, out SafeFileHandle outputWriteHandle, asyncRead: true);
+ SafeFileHandle.CreateAnonymousPipe(out SafeFileHandle errorReadHandle, out SafeFileHandle errorWriteHandle, asyncRead: true);
using (outputReadHandle)
using (outputWriteHandle)
@@ -159,10 +159,7 @@ public void ProcessStartedWithAnonymousPipeHandles_CanCaptureOutput()
outputWriteHandle.Close();
errorWriteHandle.Close();
- string stdout = process.StandardOutput.ReadToEnd();
- string stderr = process.StandardError.ReadToEnd();
-
- process.WaitForExit(WaitInMS);
+ (string stdout, string stderr) = process.ReadAllText();
Assert.Equal("stdout_hello", stdout);
Assert.Equal("stderr_hello", stderr);