Skip to content

Commit 17f52df

Browse files
CopilotadamsitnikCopilot
authored
Implement Process.ReadAllText and ReadAllBytes with platform-specific multiplexing (#126807)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> Co-authored-by: Adam Sitnik <adam.sitnik@gmail.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent d8f8562 commit 17f52df

File tree

7 files changed

+1045
-0
lines changed

7 files changed

+1045
-0
lines changed

src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,8 @@ public void Kill() { }
157157
public void Kill(bool entireProcessTree) { }
158158
public static void LeaveDebugMode() { }
159159
protected void OnExited() { }
160+
public (byte[] StandardOutput, byte[] StandardError) ReadAllBytes(System.TimeSpan? timeout = default(System.TimeSpan?)) { throw null; }
161+
public (string StandardOutput, string StandardError) ReadAllText(System.TimeSpan? timeout = default(System.TimeSpan?)) { throw null; }
160162
public void Refresh() { }
161163
[System.Runtime.Versioning.UnsupportedOSPlatformAttribute("ios")]
162164
[System.Runtime.Versioning.UnsupportedOSPlatformAttribute("tvos")]

src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
<Compile Include="System\Diagnostics\AsyncStreamReader.cs" />
2121
<Compile Include="System\Diagnostics\DataReceivedEventArgs.cs" />
2222
<Compile Include="System\Diagnostics\Process.cs" />
23+
<Compile Include="System\Diagnostics\Process.Multiplexing.cs" />
2324
<Compile Include="System\Diagnostics\ProcessExitStatus.cs" />
2425
<Compile Include="System\Diagnostics\ProcessInfo.cs" />
2526
<Compile Include="System\Diagnostics\ProcessModule.cs" />
@@ -224,6 +225,13 @@
224225
<Compile Include="Microsoft\Win32\SafeHandles\SafeProcessHandle.Windows.cs" />
225226
<Compile Include="System\Diagnostics\PerformanceCounterLib.cs" />
226227
<Compile Include="System\Diagnostics\Process.Windows.cs" />
228+
<Compile Include="System\Diagnostics\Process.Multiplexing.Windows.cs" />
229+
<Compile Include="$(CommonPath)Interop\Windows\Kernel32\Interop.ReadFile_SafeHandle_NativeOverlapped.cs"
230+
Link="Common\Interop\Windows\Kernel32\Interop.ReadFile_SafeHandle_NativeOverlapped.cs" />
231+
<Compile Include="$(CommonPath)Interop\Windows\Kernel32\Interop.GetOverlappedResult.cs"
232+
Link="Common\Interop\Windows\Kernel32\Interop.GetOverlappedResult.cs" />
233+
<Compile Include="$(CommonPath)Interop\Windows\Kernel32\Interop.CancelIoEx.cs"
234+
Link="Common\Interop\Windows\Kernel32\Interop.CancelIoEx.cs" />
227235
<Compile Include="System\Diagnostics\ProcessManager.Windows.cs" />
228236
<Compile Include="System\Diagnostics\ProcessStartInfo.Windows.cs" />
229237
<Compile Include="System\Diagnostics\ProcessThread.Windows.cs" />
@@ -236,6 +244,7 @@
236244
<ItemGroup Condition="'$(TargetPlatformIdentifier)' != '' and '$(TargetPlatformIdentifier)' != 'windows'">
237245
<Compile Include="Microsoft\Win32\SafeHandles\SafeProcessHandle.Unix.cs" />
238246
<Compile Include="System\Diagnostics\Process.Unix.cs" />
247+
<Compile Include="System\Diagnostics\Process.Multiplexing.Unix.cs" />
239248
<Compile Include="System\Diagnostics\ProcessManager.Unix.cs" />
240249
<Compile Include="System\Diagnostics\ProcessThread.Unix.cs" />
241250
<Compile Include="System\Diagnostics\ProcessStartInfo.Unix.cs" />
@@ -246,6 +255,14 @@
246255
Link="Common\Interop\Unix\Interop.Libraries.cs" />
247256
<Compile Include="$(CommonPath)Interop\Unix\Interop.Errors.cs"
248257
Link="Common\Interop\Unix\Interop.Errors.cs" />
258+
<Compile Include="$(CommonPath)Interop\Unix\Interop.Poll.Structs.cs"
259+
Link="Common\Interop\Unix\Interop.Poll.Structs.cs" />
260+
<Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.Poll.cs"
261+
Link="Common\Interop\Unix\System.Native\Interop.Poll.cs" />
262+
<Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.Fcntl.cs"
263+
Link="Common\Interop\Unix\System.Native\Interop.Fcntl.cs" />
264+
<Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.Read.cs"
265+
Link="Common\Interop\Unix\System.Native\Interop.Read.cs" />
249266
<Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.Close.cs"
250267
Link="Common\Interop\Unix\Interop.Close.cs" />
251268
<Compile Include="$(CommonPath)Interop\Unix\Interop.DefaultPathBufferSize.cs"
@@ -406,6 +423,7 @@
406423
<ProjectReference Include="$(LibrariesProjectRoot)System.Runtime.InteropServices\src\System.Runtime.InteropServices.csproj" />
407424
<ProjectReference Include="$(LibrariesProjectRoot)System.Text.Encoding.Extensions\src\System.Text.Encoding.Extensions.csproj" />
408425
<ProjectReference Include="$(LibrariesProjectRoot)System.Threading\src\System.Threading.csproj" />
426+
<ProjectReference Include="$(LibrariesProjectRoot)System.Threading.Overlapped\src\System.Threading.Overlapped.csproj" />
409427
<ProjectReference Include="$(LibrariesProjectRoot)System.Threading.Thread\src\System.Threading.Thread.csproj" />
410428
<ProjectReference Include="$(LibrariesProjectRoot)System.Threading.ThreadPool\src\System.Threading.ThreadPool.csproj" />
411429
</ItemGroup>
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.ComponentModel;
5+
using System.Runtime.InteropServices;
6+
using Microsoft.Win32.SafeHandles;
7+
8+
namespace System.Diagnostics
9+
{
10+
public partial class Process
11+
{
12+
/// <summary>
13+
/// Reads from both standard output and standard error pipes using Unix poll-based multiplexing
14+
/// with non-blocking reads.
15+
/// </summary>
16+
private static void ReadPipes(
17+
SafeFileHandle outputHandle,
18+
SafeFileHandle errorHandle,
19+
int timeoutMs,
20+
ref byte[] outputBuffer,
21+
ref int outputBytesRead,
22+
ref byte[] errorBuffer,
23+
ref int errorBytesRead)
24+
{
25+
int outputFd = outputHandle.DangerousGetHandle().ToInt32();
26+
int errorFd = errorHandle.DangerousGetHandle().ToInt32();
27+
28+
if (Interop.Sys.Fcntl.DangerousSetIsNonBlocking((IntPtr)outputFd, 1) != 0 || Interop.Sys.Fcntl.DangerousSetIsNonBlocking((IntPtr)errorFd, 1) != 0)
29+
{
30+
throw new Win32Exception();
31+
}
32+
33+
Span<Interop.PollEvent> pollFds = stackalloc Interop.PollEvent[2];
34+
35+
long deadline = timeoutMs >= 0
36+
? Environment.TickCount64 + timeoutMs
37+
: long.MaxValue;
38+
39+
bool outputDone = false, errorDone = false;
40+
while (!outputDone || !errorDone)
41+
{
42+
int numFds = 0;
43+
44+
int outputIndex = -1;
45+
int errorIndex = -1;
46+
47+
if (!outputDone)
48+
{
49+
outputIndex = numFds;
50+
pollFds[numFds].FileDescriptor = outputFd;
51+
pollFds[numFds].Events = Interop.PollEvents.POLLIN;
52+
pollFds[numFds].TriggeredEvents = Interop.PollEvents.POLLNONE;
53+
numFds++;
54+
}
55+
56+
if (!errorDone)
57+
{
58+
errorIndex = numFds;
59+
pollFds[numFds].FileDescriptor = errorFd;
60+
pollFds[numFds].Events = Interop.PollEvents.POLLIN;
61+
pollFds[numFds].TriggeredEvents = Interop.PollEvents.POLLNONE;
62+
numFds++;
63+
}
64+
65+
int pollTimeout;
66+
if (!TryGetRemainingTimeout(deadline, timeoutMs, out pollTimeout))
67+
{
68+
throw new TimeoutException();
69+
}
70+
71+
unsafe
72+
{
73+
uint triggered;
74+
fixed (Interop.PollEvent* pPollFds = pollFds)
75+
{
76+
Interop.Error error = Interop.Sys.Poll(pPollFds, (uint)numFds, pollTimeout, &triggered);
77+
if (error != Interop.Error.SUCCESS)
78+
{
79+
if (error == Interop.Error.EINTR)
80+
{
81+
// We don't re-issue the poll immediately because we need to check
82+
// if we've already exceeded the overall timeout.
83+
continue;
84+
}
85+
86+
throw new Win32Exception(Interop.Sys.ConvertErrorPalToPlatform(error));
87+
}
88+
89+
if (triggered == 0)
90+
{
91+
throw new TimeoutException();
92+
}
93+
}
94+
}
95+
96+
for (int i = 0; i < numFds; i++)
97+
{
98+
if (pollFds[i].TriggeredEvents == Interop.PollEvents.POLLNONE)
99+
{
100+
continue;
101+
}
102+
103+
bool isError = i == errorIndex;
104+
SafeFileHandle currentHandle = isError ? errorHandle : outputHandle;
105+
ref byte[] currentBuffer = ref (isError ? ref errorBuffer : ref outputBuffer);
106+
ref int currentBytesRead = ref (isError ? ref errorBytesRead : ref outputBytesRead);
107+
ref bool currentDone = ref (isError ? ref errorDone : ref outputDone);
108+
109+
int bytesRead = ReadNonBlocking(currentHandle, currentBuffer, currentBytesRead);
110+
if (bytesRead > 0)
111+
{
112+
currentBytesRead += bytesRead;
113+
114+
if (currentBytesRead == currentBuffer.Length)
115+
{
116+
RentLargerBuffer(ref currentBuffer, currentBytesRead);
117+
}
118+
}
119+
else if (bytesRead == 0)
120+
{
121+
// EOF: pipe write end was closed.
122+
currentDone = true;
123+
}
124+
// bytesRead < 0 means EAGAIN — nothing available yet, let poll retry.
125+
}
126+
}
127+
}
128+
129+
/// <summary>
130+
/// Performs a non-blocking read from the given handle into the buffer starting at the specified offset.
131+
/// Returns the number of bytes read, 0 for EOF, or -1 for EAGAIN (nothing available yet).
132+
/// </summary>
133+
private static unsafe int ReadNonBlocking(SafeFileHandle handle, byte[] buffer, int offset)
134+
{
135+
fixed (byte* pBuffer = buffer)
136+
{
137+
int bytesRead = Interop.Sys.Read(handle, pBuffer + offset, buffer.Length - offset);
138+
if (bytesRead < 0)
139+
{
140+
Interop.ErrorInfo errorInfo = Interop.Sys.GetLastErrorInfo();
141+
if (errorInfo.Error == Interop.Error.EAGAIN)
142+
{
143+
return -1;
144+
}
145+
146+
throw new Win32Exception(errorInfo.RawErrno);
147+
}
148+
149+
return bytesRead;
150+
}
151+
}
152+
}
153+
}

0 commit comments

Comments
 (0)