Skip to content

Commit 8cb4e93

Browse files
authored
Override ReadAsync and WriteAsync methods on ConsoleStream. (#71971)
* Override ReadAsync and WriteAsync methods on ConsoleStream. The base Stream class implements these overloads by renting a buffer, creating a ReadWriteTask, and copying data as necessary. Instead, ConsoleStreams can just override these Async methods and synchronously call the underlying OS API. Add tests to verify that input and output console streams behave correctly. * Fix stdin tests to only run on supported platforms.
1 parent c5005e0 commit 8cb4e93

File tree

6 files changed

+196
-14
lines changed

6 files changed

+196
-14
lines changed

src/libraries/System.Console/src/System/IO/ConsoleStream.cs

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
using System.Diagnostics;
55
using System.Runtime.InteropServices;
6+
using System.Threading;
7+
using System.Threading.Tasks;
68

79
namespace System.IO
810
{
@@ -28,6 +30,46 @@ public override void Write(byte[] buffer, int offset, int count)
2830

2931
public override void WriteByte(byte value) => Write(new ReadOnlySpan<byte>(in value));
3032

33+
public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
34+
{
35+
ValidateWrite(buffer, offset, count);
36+
37+
if (cancellationToken.IsCancellationRequested)
38+
{
39+
return Task.FromCanceled(cancellationToken);
40+
}
41+
42+
try
43+
{
44+
Write(new ReadOnlySpan<byte>(buffer, offset, count));
45+
return Task.CompletedTask;
46+
}
47+
catch (Exception ex)
48+
{
49+
return Task.FromException(ex);
50+
}
51+
}
52+
53+
public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default)
54+
{
55+
ValidateCanWrite();
56+
57+
if (cancellationToken.IsCancellationRequested)
58+
{
59+
return ValueTask.FromCanceled(cancellationToken);
60+
}
61+
62+
try
63+
{
64+
Write(buffer.Span);
65+
return ValueTask.CompletedTask;
66+
}
67+
catch (Exception ex)
68+
{
69+
return ValueTask.FromException(ex);
70+
}
71+
}
72+
3173
public override int Read(byte[] buffer, int offset, int count)
3274
{
3375
ValidateRead(buffer, offset, count);
@@ -41,6 +83,44 @@ public override int ReadByte()
4183
return result != 0 ? b : -1;
4284
}
4385

86+
public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
87+
{
88+
ValidateRead(buffer, offset, count);
89+
90+
if (cancellationToken.IsCancellationRequested)
91+
{
92+
return Task.FromCanceled<int>(cancellationToken);
93+
}
94+
95+
try
96+
{
97+
return Task.FromResult(Read(new Span<byte>(buffer, offset, count)));
98+
}
99+
catch (Exception exception)
100+
{
101+
return Task.FromException<int>(exception);
102+
}
103+
}
104+
105+
public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
106+
{
107+
ValidateCanRead();
108+
109+
if (cancellationToken.IsCancellationRequested)
110+
{
111+
return ValueTask.FromCanceled<int>(cancellationToken);
112+
}
113+
114+
try
115+
{
116+
return ValueTask.FromResult(Read(buffer.Span));
117+
}
118+
catch (Exception exception)
119+
{
120+
return ValueTask.FromException<int>(exception);
121+
}
122+
}
123+
44124
protected override void Dispose(bool disposing)
45125
{
46126
_canRead = false;
@@ -74,7 +154,11 @@ public override void Flush()
74154
protected void ValidateRead(byte[] buffer, int offset, int count)
75155
{
76156
ValidateBufferArguments(buffer, offset, count);
157+
ValidateCanRead();
158+
}
77159

160+
private void ValidateCanRead()
161+
{
78162
if (!_canRead)
79163
{
80164
throw Error.GetReadNotSupported();
@@ -84,7 +168,11 @@ protected void ValidateRead(byte[] buffer, int offset, int count)
84168
protected void ValidateWrite(byte[] buffer, int offset, int count)
85169
{
86170
ValidateBufferArguments(buffer, offset, count);
171+
ValidateCanWrite();
172+
}
87173

174+
private void ValidateCanWrite()
175+
{
88176
if (!_canWrite)
89177
{
90178
throw Error.GetWriteNotSupported();
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
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;
5+
using System.IO;
6+
using System.Text;
7+
using System.Threading;
8+
using System.Threading.Tasks;
9+
using Xunit;
10+
11+
public class ConsoleStreamTests
12+
{
13+
[Fact]
14+
public void WriteToOutputStream_EmptyArray()
15+
{
16+
Stream outStream = Console.OpenStandardOutput();
17+
outStream.Write(new byte[] { }, 0, 0);
18+
}
19+
20+
[ConditionalFact(typeof(Helpers), nameof(Helpers.IsConsoleInSupported))]
21+
public void ReadAsyncRespectsCancellation()
22+
{
23+
Stream inStream = Console.OpenStandardInput();
24+
CancellationTokenSource cts = new CancellationTokenSource();
25+
cts.Cancel();
26+
27+
byte[] buffer = new byte[1024];
28+
Task result = inStream.ReadAsync(buffer, 0, buffer.Length, cts.Token);
29+
Assert.True(result.IsCanceled);
30+
31+
ValueTask<int> valueTaskResult = inStream.ReadAsync(buffer.AsMemory(), cts.Token);
32+
Assert.True(valueTaskResult.IsCanceled);
33+
}
34+
35+
[ConditionalFact(typeof(Helpers), nameof(Helpers.IsConsoleInSupported))]
36+
public void ReadAsyncHandlesInvalidParams()
37+
{
38+
Stream inStream = Console.OpenStandardInput();
39+
40+
byte[] buffer = new byte[1024];
41+
Assert.Throws<ArgumentNullException>(() => { inStream.ReadAsync(null, 0, buffer.Length); });
42+
Assert.Throws<ArgumentOutOfRangeException>(() => { inStream.ReadAsync(buffer, -1, buffer.Length); });
43+
Assert.Throws<ArgumentOutOfRangeException>(() => { inStream.ReadAsync(buffer, 0, buffer.Length + 1); });
44+
}
45+
46+
[Fact]
47+
public void WriteAsyncRespectsCancellation()
48+
{
49+
Stream outStream = Console.OpenStandardOutput();
50+
CancellationTokenSource cts = new CancellationTokenSource();
51+
cts.Cancel();
52+
53+
byte[] bytes = Encoding.ASCII.GetBytes("Hi");
54+
Task result = outStream.WriteAsync(bytes, 0, bytes.Length, cts.Token);
55+
Assert.True(result.IsCanceled);
56+
57+
ValueTask valueTaskResult = outStream.WriteAsync(bytes.AsMemory(), cts.Token);
58+
Assert.True(valueTaskResult.IsCanceled);
59+
}
60+
61+
[Fact]
62+
public void WriteAsyncHandlesInvalidParams()
63+
{
64+
Stream outStream = Console.OpenStandardOutput();
65+
66+
byte[] bytes = Encoding.ASCII.GetBytes("Hi");
67+
Assert.Throws<ArgumentNullException>(() => { outStream.WriteAsync(null, 0, bytes.Length); });
68+
Assert.Throws<ArgumentOutOfRangeException>(() => { outStream.WriteAsync(bytes, -1, bytes.Length); });
69+
Assert.Throws<ArgumentOutOfRangeException>(() => { outStream.WriteAsync(bytes, 0, bytes.Length + 1); });
70+
}
71+
72+
[ConditionalFact(typeof(Helpers), nameof(Helpers.IsConsoleInSupported))]
73+
public void InputCannotWriteAsync()
74+
{
75+
Stream inStream = Console.OpenStandardInput();
76+
77+
byte[] bytes = Encoding.ASCII.GetBytes("Hi");
78+
Assert.Throws<NotSupportedException>(() => { inStream.WriteAsync(bytes, 0, bytes.Length); });
79+
80+
Assert.Throws<NotSupportedException>(() => { inStream.WriteAsync(bytes.AsMemory()); });
81+
}
82+
83+
[Fact]
84+
public void OutputCannotReadAsync()
85+
{
86+
Stream outStream = Console.OpenStandardOutput();
87+
88+
byte[] buffer = new byte[1024];
89+
Assert.Throws<NotSupportedException>(() =>
90+
{
91+
outStream.ReadAsync(buffer, 0, buffer.Length);
92+
});
93+
94+
Assert.Throws<NotSupportedException>(() =>
95+
{
96+
outStream.ReadAsync(buffer.AsMemory());
97+
});
98+
}
99+
}

src/libraries/System.Console/tests/Helpers.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,11 @@
66
using System.Text;
77
using Xunit;
88

9-
class Helpers
9+
static class Helpers
1010
{
11+
public static bool IsConsoleInSupported =>
12+
!PlatformDetection.IsAndroid && !PlatformDetection.IsiOS && !PlatformDetection.IsMacCatalyst && !PlatformDetection.IstvOS && !PlatformDetection.IsBrowser;
13+
1114
public static void SetAndReadHelper(Action<TextWriter> setHelper, Func<TextWriter> getHelper, Func<StreamReader, string> readHelper)
1215
{
1316
const string TestString = "Test";

src/libraries/System.Console/tests/ReadAndWrite.cs

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,6 @@ public static void WriteOverloads()
3030
}
3131
}
3232

33-
[Fact]
34-
public static void WriteToOutputStream_EmptyArray()
35-
{
36-
Stream outStream = Console.OpenStandardOutput();
37-
outStream.Write(new byte[] { }, 0, 0);
38-
}
39-
4033
[Fact]
4134
[OuterLoop]
4235
public static void WriteOverloadsToRealConsole()

src/libraries/System.Console/tests/SetIn.cs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,7 @@
1010
//
1111
public class SetIn
1212
{
13-
[Fact]
14-
[SkipOnPlatform(TestPlatforms.Browser | TestPlatforms.iOS | TestPlatforms.MacCatalyst | TestPlatforms.tvOS, "Not supported on Browser, iOS, MacCatalyst, or tvOS.")]
13+
[ConditionalFact(typeof(Helpers), nameof(Helpers.IsConsoleInSupported))]
1514
public static void SetInThrowsOnNull()
1615
{
1716
TextReader savedIn = Console.In;
@@ -25,8 +24,7 @@ public static void SetInThrowsOnNull()
2524
}
2625
}
2726

28-
[Fact]
29-
[SkipOnPlatform(TestPlatforms.Browser | TestPlatforms.iOS | TestPlatforms.MacCatalyst | TestPlatforms.tvOS, "Not supported on Browser, iOS, MacCatalyst, or tvOS.")]
27+
[ConditionalFact(typeof(Helpers), nameof(Helpers.IsConsoleInSupported))]
3028
public static void SetInReadLine()
3129
{
3230
const string TextStringFormat = "Test {0}";

src/libraries/System.Console/tests/System.Console.Tests.csproj

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
</PropertyGroup>
88
<ItemGroup>
99
<Compile Include="CancelKeyPress.cs" />
10+
<Compile Include="ConsoleStreamTests.cs" />
1011
<Compile Include="Helpers.cs" />
1112
<Compile Include="ReadAndWrite.cs" />
1213
<Compile Include="ConsoleKeyInfoTests.cs" />
@@ -35,8 +36,8 @@
3536
</ItemGroup>
3637
<ItemGroup>
3738
<Content Include="$(MSBuildThisFileDirectory)TestData\**\*"
38-
Link="%(RecursiveDir)%(Filename)%(Extension)"
39-
CopyToOutputDirectory="PreserveNewest" />
39+
Link="%(RecursiveDir)%(Filename)%(Extension)"
40+
CopyToOutputDirectory="PreserveNewest" />
4041
</ItemGroup>
4142
<ItemGroup Condition="'$(TargetPlatformIdentifier)' == 'windows'">
4243
<Compile Include="ConsoleEncoding.Windows.cs" />

0 commit comments

Comments
 (0)