From 1f2a0b0b1099d1842a2916b03e5544cee23765e2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Mar 2026 09:21:22 +0000 Subject: [PATCH 1/7] Initial plan From 086df300d6219f9dff66637289fcbab969022b32 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Mar 2026 09:32:20 +0000 Subject: [PATCH 2/7] Extend Process.CreatePipe with asyncReads for overlapped IO on stdout/stderr pipes Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- .../src/System/Diagnostics/Process.Windows.cs | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Windows.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Windows.cs index 8a7e2b914c4850..f51443120eca5c 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Windows.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Windows.cs @@ -18,6 +18,10 @@ public partial class Process : IDisposable { private static readonly object s_createProcessLock = new object(); + // When not disabled via the environment variable, use overlapped (async) I/O for the parent's end + // of stdout/stderr pipes so that reads don't tie up a thread-pool thread per pipe instance. + private static readonly bool s_useAsyncReads = Environment.GetEnvironmentVariable("DOTNET_SYSTEM_DIAGNOSTICS_PROCESS_DISABLE_ASYNC_READ") != "true"; + private string? _processName; /// @@ -464,7 +468,7 @@ private unsafe bool StartWithCreateProcess(ProcessStartInfo startInfo) { if (startInfo.RedirectStandardInput) { - CreatePipe(out parentInputPipeHandle, out childInputPipeHandle, true); + CreatePipe(out parentInputPipeHandle, out childInputPipeHandle, true, asyncReads: false); } else { @@ -473,7 +477,7 @@ private unsafe bool StartWithCreateProcess(ProcessStartInfo startInfo) if (startInfo.RedirectStandardOutput) { - CreatePipe(out parentOutputPipeHandle, out childOutputPipeHandle, false); + CreatePipe(out parentOutputPipeHandle, out childOutputPipeHandle, false, asyncReads: s_useAsyncReads); } else { @@ -482,7 +486,7 @@ private unsafe bool StartWithCreateProcess(ProcessStartInfo startInfo) if (startInfo.RedirectStandardError) { - CreatePipe(out parentErrorPipeHandle, out childErrorPipeHandle, false); + CreatePipe(out parentErrorPipeHandle, out childErrorPipeHandle, false, asyncReads: s_useAsyncReads); } else { @@ -807,9 +811,14 @@ private SafeProcessHandle GetProcessHandle(int access, bool throwIfExited = true // methods such as WriteLine as well as native CRT functions like printf) which are making an // assumption that the console standard handles (obtained via GetStdHandle()) are opened // for synchronous I/O and hence they can work fine with ReadFile/WriteFile synchronously! - private static void CreatePipe(out SafeFileHandle parentHandle, out SafeFileHandle childHandle, bool parentInputs) + // We therefore only open the parent's end of the pipe for async I/O (overlapped), while the + // child's end is always opened for synchronous I/O so the child process can use it normally. + private static void CreatePipe(out SafeFileHandle parentHandle, out SafeFileHandle childHandle, bool parentInputs, bool asyncReads) { - SafeFileHandle.CreateAnonymousPipe(out SafeFileHandle readHandle, out SafeFileHandle writeHandle); + // Only the parent's read end benefits from async I/O; stdin is always sync. + // asyncRead applies to the read handle; asyncWrite to the write handle. + bool asyncRead = !parentInputs && asyncReads; + SafeFileHandle.CreateAnonymousPipe(out SafeFileHandle readHandle, out SafeFileHandle writeHandle, asyncRead: asyncRead); // parentInputs=true: parent writes to pipe, child reads (stdin redirect). // parentInputs=false: parent reads from pipe, child writes (stdout/stderr redirect). From ac375196a5362bfb6c166885ef4928f81ca4f78e Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Tue, 17 Mar 2026 10:40:19 +0100 Subject: [PATCH 3/7] Apply suggestions from code review Co-authored-by: Adam Sitnik --- .../src/System/Diagnostics/Process.Windows.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Windows.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Windows.cs index f51443120eca5c..742db49b7e7577 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Windows.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Windows.cs @@ -20,7 +20,7 @@ public partial class Process : IDisposable // When not disabled via the environment variable, use overlapped (async) I/O for the parent's end // of stdout/stderr pipes so that reads don't tie up a thread-pool thread per pipe instance. - private static readonly bool s_useAsyncReads = Environment.GetEnvironmentVariable("DOTNET_SYSTEM_DIAGNOSTICS_PROCESS_DISABLE_ASYNC_READ") != "true"; + private static readonly bool s_useAsyncReads = Environment.GetEnvironmentVariable("DOTNET_SYSTEM_DIAGNOSTICS_PROCESS_DISABLE_ASYNC_READS") != "true"; private string? _processName; @@ -818,7 +818,7 @@ private static void CreatePipe(out SafeFileHandle parentHandle, out SafeFileHand // Only the parent's read end benefits from async I/O; stdin is always sync. // asyncRead applies to the read handle; asyncWrite to the write handle. bool asyncRead = !parentInputs && asyncReads; - SafeFileHandle.CreateAnonymousPipe(out SafeFileHandle readHandle, out SafeFileHandle writeHandle, asyncRead: asyncRead); + SafeFileHandle.CreateAnonymousPipe(out SafeFileHandle readHandle, out SafeFileHandle writeHandle, asyncRead: asyncRead, asyncWrite: false); // parentInputs=true: parent writes to pipe, child reads (stdin redirect). // parentInputs=false: parent reads from pipe, child writes (stdout/stderr redirect). From 7ed78ce6946fd567d88387eddd7e96c241d37d60 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Tue, 17 Mar 2026 19:24:03 +0100 Subject: [PATCH 4/7] ensure CreateAnonymousPipe mimics CreatePipe for async pipes: - we have to specify inBufferSize, otherwise the first write always blocks until reading starts - set the same timeout as CreatePipe (120s) --- .../Win32/SafeHandles/SafeFileHandle.Windows.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs index 3d5f8d22db02ba..ec10b0999cd4fb 100644 --- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs @@ -65,7 +65,15 @@ public static partial void CreateAnonymousPipe(out SafeFileHandle readHandle, ou const int pipeMode = (int)(Interop.Kernel32.PipeOptions.PIPE_TYPE_BYTE | Interop.Kernel32.PipeOptions.PIPE_READMODE_BYTE); // Data is read from the pipe as a stream of bytes // We could consider specifying a larger buffer size. - tempReadHandle = Interop.Kernel32.CreateNamedPipeFileHandle(pipeName, openMode, pipeMode, 1, 0, 0, 0, ref securityAttributes); + tempReadHandle = Interop.Kernel32.CreateNamedPipeFileHandle( + pipeName, + openMode, + pipeMode, + maxInstances: 1, // we don't want anybody else to open this pipe + outBufferSize: 0, // unused (we use it only for reading) + inBufferSize: 4 * 4096, // CreatePipe uses a 4096 buffer by default, we use bigger buffer for better performance + defaultTimeout: (int)TimeSpan.FromSeconds(120).TotalMilliseconds, // same as the default for CreatePipe + ref securityAttributes); try { From a33287dd9d6ffbf2a917de2265ba6dbf88d26cc7 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Wed, 18 Mar 2026 12:25:00 +0100 Subject: [PATCH 5/7] address code review feedback --- .../src/System/Diagnostics/Process.Windows.cs | 12 ++--- .../tests/ProcessStandardConsoleTests.cs | 44 +++++++++++++++++++ 2 files changed, 48 insertions(+), 8 deletions(-) diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Windows.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Windows.cs index 742db49b7e7577..384575cc6d4a2c 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Windows.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Windows.cs @@ -18,10 +18,6 @@ public partial class Process : IDisposable { private static readonly object s_createProcessLock = new object(); - // When not disabled via the environment variable, use overlapped (async) I/O for the parent's end - // of stdout/stderr pipes so that reads don't tie up a thread-pool thread per pipe instance. - private static readonly bool s_useAsyncReads = Environment.GetEnvironmentVariable("DOTNET_SYSTEM_DIAGNOSTICS_PROCESS_DISABLE_ASYNC_READS") != "true"; - private string? _processName; /// @@ -477,7 +473,7 @@ private unsafe bool StartWithCreateProcess(ProcessStartInfo startInfo) if (startInfo.RedirectStandardOutput) { - CreatePipe(out parentOutputPipeHandle, out childOutputPipeHandle, false, asyncReads: s_useAsyncReads); + CreatePipe(out parentOutputPipeHandle, out childOutputPipeHandle, false, asyncReads: true); } else { @@ -486,7 +482,7 @@ private unsafe bool StartWithCreateProcess(ProcessStartInfo startInfo) if (startInfo.RedirectStandardError) { - CreatePipe(out parentErrorPipeHandle, out childErrorPipeHandle, false, asyncReads: s_useAsyncReads); + CreatePipe(out parentErrorPipeHandle, out childErrorPipeHandle, false, asyncReads: true); } else { @@ -644,12 +640,12 @@ ref processInfo // pointer to PROCESS_INFORMATION if (startInfo.RedirectStandardOutput) { Encoding enc = startInfo.StandardOutputEncoding ?? GetEncoding((int)Interop.Kernel32.GetConsoleOutputCP()); - _standardOutput = new StreamReader(new FileStream(parentOutputPipeHandle!, FileAccess.Read, 4096, false), enc, true, 4096); + _standardOutput = new StreamReader(new FileStream(parentOutputPipeHandle!, FileAccess.Read, 4096, parentOutputPipeHandle!.IsAsync), enc, true, 4096); } if (startInfo.RedirectStandardError) { Encoding enc = startInfo.StandardErrorEncoding ?? GetEncoding((int)Interop.Kernel32.GetConsoleOutputCP()); - _standardError = new StreamReader(new FileStream(parentErrorPipeHandle!, FileAccess.Read, 4096, false), enc, true, 4096); + _standardError = new StreamReader(new FileStream(parentErrorPipeHandle!, FileAccess.Read, 4096, parentErrorPipeHandle!.IsAsync), enc, true, 4096); } commandLine.Dispose(); diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessStandardConsoleTests.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessStandardConsoleTests.cs index b55384a1606527..d89f4909505e35 100644 --- a/src/libraries/System.Diagnostics.Process/tests/ProcessStandardConsoleTests.cs +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessStandardConsoleTests.cs @@ -1,9 +1,12 @@ // 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.IO.Pipes; using System.Text; using Microsoft.DotNet.RemoteExecutor; using Microsoft.Win32; +using Microsoft.Win32.SafeHandles; using Xunit; namespace System.Diagnostics.Tests @@ -55,5 +58,46 @@ void RunWithExpectedCodePage(int expectedCodePage) Interop.SetConsoleOutputCP(outputEncoding); } } + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public void Start_Redirect_StandardHandles_UseRightAsyncMode() + { + using (Process process = CreateProcess(() => + { + Assert.False(Console.OpenStandardInputHandle().IsAsync); + Assert.False(Console.OpenStandardOutputHandle().IsAsync); + Assert.False(Console.OpenStandardErrorHandle().IsAsync); + + return RemoteExecutor.SuccessExitCode; + })) + { + process.StartInfo.RedirectStandardInput = true; + process.StartInfo.RedirectStandardOutput = true; + process.StartInfo.RedirectStandardError = true; + + Assert.True(process.Start()); + + Assert.False(GetSafeFileHandle(process.StandardInput.BaseStream).IsAsync); + Assert.Equal(OperatingSystem.IsWindows(), GetSafeFileHandle(process.StandardOutput.BaseStream).IsAsync); + Assert.Equal(OperatingSystem.IsWindows(), GetSafeFileHandle(process.StandardError.BaseStream).IsAsync); + + process.WaitForExit(); // ensure event handlers have completed + Assert.Equal(RemoteExecutor.SuccessExitCode, process.ExitCode); + } + + static SafeFileHandle GetSafeFileHandle(Stream baseStream) + { + switch (baseStream) + { + case FileStream fileStream: + return fileStream.SafeFileHandle; + case AnonymousPipeClientStream anonymousPipeStream: + SafePipeHandle safePipeHandle = anonymousPipeStream.SafePipeHandle; + return new SafeFileHandle(safePipeHandle.DangerousGetHandle(), ownsHandle: false); + default: + throw new NotSupportedException(); + } + } + } } } From 369b80f9cbd99926a75cc5547f61a628043359b2 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Wed, 18 Mar 2026 13:07:21 +0100 Subject: [PATCH 6/7] make sure SafeFileHandle.CreateAnonymousPipe is going to work in Windows AppContainers by using the `\\.\pipe\LOCAL\` path prefix --- .../Win32/SafeHandles/SafeFileHandle.Windows.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs index ec10b0999cd4fb..c57dcab8130639 100644 --- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs @@ -46,7 +46,14 @@ public static partial void CreateAnonymousPipe(out SafeFileHandle readHandle, ou else { // When one or both ends are async, use named pipes to support async I/O. - string pipeName = $@"\\.\pipe\dotnet_{Guid.NewGuid():N}"; + + // From https://learn.microsoft.com/windows/win32/api/winbase/nf-winbase-createnamedpipea#remarks: + // "Windows 10, version 1709: Pipes are only supported within an app-container; ie, + // from one UWP process to another UWP process that's part of the same app. + // Also, named pipes must use the syntax \\.\pipe\LOCAL\ for the pipe name." + // That is why we use "LOCAL" namespace for the pipe name, + // so that it works in AppContainer and outside of it. + string pipeName = $@"\\.\pipe\LOCAL\dotnet_{Guid.NewGuid():N}"; // Security: we don't need to specify a security descriptor, because // we allow only for 1 instance of the pipe and immediately open the write end, From 659e7c6a44120e379c75fc16524bd3ab3245dfef Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Wed, 18 Mar 2026 13:54:12 +0100 Subject: [PATCH 7/7] address code review feedback --- .../src/System/Diagnostics/Process.Windows.cs | 11 +++++------ .../Win32/SafeHandles/SafeFileHandle.Windows.cs | 1 - 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Windows.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Windows.cs index 384575cc6d4a2c..f073bd1e8d54f5 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Windows.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Windows.cs @@ -464,7 +464,7 @@ private unsafe bool StartWithCreateProcess(ProcessStartInfo startInfo) { if (startInfo.RedirectStandardInput) { - CreatePipe(out parentInputPipeHandle, out childInputPipeHandle, true, asyncReads: false); + CreatePipe(out parentInputPipeHandle, out childInputPipeHandle, true); } else { @@ -473,7 +473,7 @@ private unsafe bool StartWithCreateProcess(ProcessStartInfo startInfo) if (startInfo.RedirectStandardOutput) { - CreatePipe(out parentOutputPipeHandle, out childOutputPipeHandle, false, asyncReads: true); + CreatePipe(out parentOutputPipeHandle, out childOutputPipeHandle, false); } else { @@ -482,7 +482,7 @@ private unsafe bool StartWithCreateProcess(ProcessStartInfo startInfo) if (startInfo.RedirectStandardError) { - CreatePipe(out parentErrorPipeHandle, out childErrorPipeHandle, false, asyncReads: true); + CreatePipe(out parentErrorPipeHandle, out childErrorPipeHandle, false); } else { @@ -809,12 +809,11 @@ private SafeProcessHandle GetProcessHandle(int access, bool throwIfExited = true // for synchronous I/O and hence they can work fine with ReadFile/WriteFile synchronously! // We therefore only open the parent's end of the pipe for async I/O (overlapped), while the // child's end is always opened for synchronous I/O so the child process can use it normally. - private static void CreatePipe(out SafeFileHandle parentHandle, out SafeFileHandle childHandle, bool parentInputs, bool asyncReads) + private static void CreatePipe(out SafeFileHandle parentHandle, out SafeFileHandle childHandle, bool parentInputs) { // Only the parent's read end benefits from async I/O; stdin is always sync. // asyncRead applies to the read handle; asyncWrite to the write handle. - bool asyncRead = !parentInputs && asyncReads; - SafeFileHandle.CreateAnonymousPipe(out SafeFileHandle readHandle, out SafeFileHandle writeHandle, asyncRead: asyncRead, asyncWrite: false); + SafeFileHandle.CreateAnonymousPipe(out SafeFileHandle readHandle, out SafeFileHandle writeHandle, asyncRead: !parentInputs, asyncWrite: false); // parentInputs=true: parent writes to pipe, child reads (stdin redirect). // parentInputs=false: parent reads from pipe, child writes (stdout/stderr redirect). diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs index c57dcab8130639..dad993147493cb 100644 --- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs @@ -71,7 +71,6 @@ public static partial void CreateAnonymousPipe(out SafeFileHandle readHandle, ou const int pipeMode = (int)(Interop.Kernel32.PipeOptions.PIPE_TYPE_BYTE | Interop.Kernel32.PipeOptions.PIPE_READMODE_BYTE); // Data is read from the pipe as a stream of bytes - // We could consider specifying a larger buffer size. tempReadHandle = Interop.Kernel32.CreateNamedPipeFileHandle( pipeName, openMode,