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);