Skip to content
This repository was archived by the owner on Jan 23, 2023. It is now read-only.

Commit 0f03d11

Browse files
strajkovmsftkarelz
authored andcommitted
Dispose of process streams if they are not referenced by the user. (#26204)
If the process pipes for stdin/stdout/stderr are not touched by the user, dispose of them. Partial fix to #25962
1 parent 697e2b9 commit 0f03d11

File tree

3 files changed

+118
-10
lines changed

3 files changed

+118
-10
lines changed

src/System.Diagnostics.Process/src/System/Diagnostics/Process.cs

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ public partial class Process : Component
6767

6868
private static object s_createProcessLock = new object();
6969

70+
private bool _standardInputAccessed;
71+
7072
private StreamReadMode _outputStreamReadMode;
7173
private StreamReadMode _errorStreamReadMode;
7274

@@ -691,6 +693,7 @@ public StreamWriter StandardInput
691693
throw new InvalidOperationException(SR.CantGetStandardIn);
692694
}
693695

696+
_standardInputAccessed = true;
694697
return _standardInput;
695698
}
696699
}
@@ -846,18 +849,45 @@ public void Close()
846849
_machineName = ".";
847850
_raisedOnExited = false;
848851

849-
//Don't call close on the Readers and writers
850-
//since they might be referenced by somebody else while the
851-
//process is still alive but this method called.
852-
_standardOutput = null;
853-
_standardInput = null;
854-
_standardError = null;
852+
// Only call close on the streams if the user cannot have a reference on them.
853+
// If they are referenced it is the user's responsibility to dispose of them.
854+
try
855+
{
856+
if (_standardOutput != null && (_outputStreamReadMode == StreamReadMode.AsyncMode || _outputStreamReadMode == StreamReadMode.Undefined))
857+
{
858+
if (_outputStreamReadMode == StreamReadMode.AsyncMode)
859+
{
860+
_output.CancelOperation();
861+
}
862+
_standardOutput.Close();
863+
}
855864

856-
_output = null;
857-
_error = null;
865+
if (_standardError != null && (_errorStreamReadMode == StreamReadMode.AsyncMode || _errorStreamReadMode == StreamReadMode.Undefined))
866+
{
867+
if (_errorStreamReadMode == StreamReadMode.AsyncMode)
868+
{
869+
_error.CancelOperation();
870+
}
871+
_standardError.Close();
872+
}
873+
874+
if (_standardInput != null && !_standardInputAccessed)
875+
{
876+
_standardInput.Close();
877+
}
878+
}
879+
finally
880+
{
881+
_standardOutput = null;
882+
_standardInput = null;
883+
_standardError = null;
858884

859-
CloseCore();
860-
Refresh();
885+
_output = null;
886+
_error = null;
887+
888+
CloseCore();
889+
Refresh();
890+
}
861891
}
862892
}
863893

src/System.Diagnostics.Process/tests/ProcessStreamReadTests.cs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,57 @@ public void TestManyOutputLines()
228228
Assert.Equal(ExpectedLineCount + 1, totalLinesReceived);
229229
}
230230

231+
[Fact]
232+
public void TestClosingStreamsAsyncDoesNotThrow()
233+
{
234+
Process p = CreateProcessPortable(RemotelyInvokable.WriteLinesAfterClose);
235+
p.StartInfo.RedirectStandardOutput = true;
236+
p.StartInfo.RedirectStandardError = true;
237+
238+
// On netfx, the handler is called once with the Data as null, even if the process writes nothing to the pipe.
239+
// That behavior is documented here https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.datareceivedeventhandler
240+
p.OutputDataReceived += (s, e) => { Assert.True(PlatformDetection.IsFullFramework && e.Data == null, "OutputDataReceived called after closing the process"); };
241+
p.ErrorDataReceived += (s, e) => { Assert.True(PlatformDetection.IsFullFramework && e.Data == null, "ErrorDataReceived called after closing the process"); };
242+
243+
p.Start();
244+
p.BeginOutputReadLine();
245+
p.BeginErrorReadLine();
246+
247+
p.Close();
248+
RemotelyInvokable.FireClosedEvent();
249+
}
250+
251+
[Fact]
252+
public void TestClosingStreamsUndefinedDoesNotThrow()
253+
{
254+
Process p = CreateProcessPortable(RemotelyInvokable.WriteLinesAfterClose);
255+
p.StartInfo.RedirectStandardOutput = true;
256+
p.StartInfo.RedirectStandardError = true;
257+
258+
p.Start();
259+
p.Close();
260+
RemotelyInvokable.FireClosedEvent();
261+
}
262+
263+
[Fact]
264+
public void TestClosingSyncModeDoesNotCloseStreams()
265+
{
266+
Process p = CreateProcessPortable(RemotelyInvokable.WriteLinesAfterClose);
267+
p.StartInfo.RedirectStandardOutput = true;
268+
p.StartInfo.RedirectStandardError = true;
269+
270+
p.Start();
271+
272+
var output = p.StandardOutput;
273+
var error = p.StandardError;
274+
275+
p.Close();
276+
RemotelyInvokable.FireClosedEvent();
277+
278+
output.ReadToEnd();
279+
error.ReadToEnd();
280+
}
281+
231282
[Fact]
232283
public void TestStreamNegativeTests()
233284
{

src/System.Diagnostics.Process/tests/RemotelyInvokable.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ internal static class RemotelyInvokable
2222
public static readonly int SuccessExitCode = 42;
2323
public const int WaitInMS = 30 * 1000;
2424
public const string TestConsoleApp = "System.Diagnostics.Process.Tests";
25+
public static event EventHandler ClosedEvent;
26+
2527

2628
public static string DummyUapCmd()
2729
{
@@ -161,6 +163,26 @@ public static int Write144Lines()
161163
return SuccessExitCode;
162164
}
163165

166+
public static int WriteLinesAfterClose()
167+
{
168+
ClosedEvent += (s, e) =>
169+
{
170+
Console.WriteLine("This is a line to output.");
171+
Console.Error.WriteLine("This is a line to error.");
172+
};
173+
return SuccessExitCode;
174+
}
175+
176+
public static string WriteLinesAfterCloseUapCmd()
177+
{
178+
ClosedEvent += (s, e) =>
179+
{
180+
// Finish the pause
181+
Console.WriteLine();
182+
};
183+
return "(pause > nul) & (echo This is a line to output) & (echo This is a line to error 1>&2)";
184+
}
185+
164186
public static string ConcatThreeArgumentsUapCmd(string one, string two, string three)
165187
{
166188
return $"echo {string.Join(",", one, two, three)} & exit {SuccessExitCode}";
@@ -182,5 +204,10 @@ public static int SelfTerminate()
182204
Process.GetCurrentProcess().Kill();
183205
throw new ShouldNotBeInvokedException();
184206
}
207+
208+
public static void FireClosedEvent()
209+
{
210+
ClosedEvent?.Invoke(null, EventArgs.Empty);
211+
}
185212
}
186213
}

0 commit comments

Comments
 (0)