Skip to content

Commit 4f72069

Browse files
committed
Add CancellationToken to TextReader.ReadXAsync. (#20824)
1 parent a5158df commit 4f72069

File tree

9 files changed

+178
-18
lines changed

9 files changed

+178
-18
lines changed

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

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

44
using System.Diagnostics;
55
using System.Runtime.InteropServices;
6+
using System.Threading;
67
using System.Threading.Tasks;
78

89
namespace System.IO
@@ -95,11 +96,21 @@ public override string ReadToEnd()
9596
return Task.FromResult(ReadLine());
9697
}
9798

99+
public override ValueTask<string?> ReadLineAsync(CancellationToken cancellationToken)
100+
{
101+
return cancellationToken.IsCancellationRequested ? ValueTask.FromCanceled<string?>(cancellationToken) : new ValueTask<string?>(ReadLine());
102+
}
103+
98104
public override Task<string> ReadToEndAsync()
99105
{
100106
return Task.FromResult(ReadToEnd());
101107
}
102108

109+
public override Task<string> ReadToEndAsync(CancellationToken cancellationToken)
110+
{
111+
return cancellationToken.IsCancellationRequested ? Task.FromCanceled<string>(cancellationToken) : Task.FromResult(ReadToEnd());
112+
}
113+
103114
public override Task<int> ReadBlockAsync(char[] buffer, int index, int count)
104115
{
105116
if (buffer == null)

src/libraries/System.IO/tests/StreamReader/StreamReaderTests.cs

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,53 @@ public async Task ReadToEndAsync()
110110
Assert.Equal(5000, result.Length);
111111
}
112112

113+
[Fact]
114+
public async Task ReadToEndAsync_WithCancellationToken()
115+
{
116+
using var sw = new StreamReader(GetLargeStream());
117+
var result = await sw.ReadToEndAsync(default);
118+
119+
Assert.Equal(5000, result.Length);
120+
}
121+
122+
[Fact]
123+
public async Task ReadToEndAsync_WithCanceledCancellationToken()
124+
{
125+
using var sw = new StreamReader(GetLargeStream());
126+
using var cts = new CancellationTokenSource();
127+
cts.Cancel();
128+
await Assert.ThrowsAnyAsync<OperationCanceledException>(async () => await sw.ReadToEndAsync(cts.Token));
129+
}
130+
131+
[Fact]
132+
public async Task ReadToEndAsync_WithCancellation()
133+
{
134+
var path = Path.GetTempFileName();
135+
try
136+
{
137+
// create large (~100MB) file
138+
using (var writer = new StreamWriter(path))
139+
{
140+
for (var i = 0; i < 1_000_000; i++)
141+
writer.WriteLine("A very large file used for testing StreamReader cancellation. 0123456789012345678901234567890123456789.");
142+
}
143+
144+
using var reader = File.OpenText(path);
145+
using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(50));
146+
await Assert.ThrowsAnyAsync<OperationCanceledException>(async () => await reader.ReadToEndAsync(cts.Token));
147+
}
148+
finally
149+
{
150+
try
151+
{
152+
File.Delete(path);
153+
}
154+
catch (Exception)
155+
{
156+
}
157+
}
158+
}
159+
113160
[Fact]
114161
public void GetBaseStream()
115162
{
@@ -301,6 +348,27 @@ public void VanillaReadLines2()
301348
Assert.Equal(valueString.Substring(1, valueString.IndexOf('\r') - 1), data);
302349
}
303350

351+
[Fact]
352+
public async Task VanillaReadLineAsync()
353+
{
354+
var baseInfo = GetCharArrayStream();
355+
var sr = baseInfo.Item2;
356+
357+
string valueString = new string(baseInfo.Item1);
358+
359+
var data = await sr.ReadLineAsync();
360+
Assert.Equal(valueString.Substring(0, valueString.IndexOf('\r')), data);
361+
362+
data = await sr.ReadLineAsync(default);
363+
Assert.Equal(valueString.Substring(valueString.IndexOf('\r') + 1, 3), data);
364+
365+
data = await sr.ReadLineAsync();
366+
Assert.Equal(valueString.Substring(valueString.IndexOf('\n') + 1, 2), data);
367+
368+
data = await sr.ReadLineAsync(default);
369+
Assert.Equal((valueString.Substring(valueString.LastIndexOf('\n') + 1)), data);
370+
}
371+
304372
[Fact]
305373
public async Task ContinuousNewLinesAndTabsAsync()
306374
{

src/libraries/System.IO/tests/StringReader/StringReader.CtorTests.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,23 @@ public static void ReadLine()
6868
}
6969
}
7070

71+
[Fact]
72+
public static async Task ReadLineAsync()
73+
{
74+
string str1 = "Hello\0\t\v \\ World";
75+
string str2 = str1 + Environment.NewLine + str1;
76+
77+
using (StringReader sr = new StringReader(str1))
78+
{
79+
Assert.Equal(str1, await sr.ReadLineAsync());
80+
}
81+
using (StringReader sr = new StringReader(str2))
82+
{
83+
Assert.Equal(str1, await sr.ReadLineAsync(default));
84+
Assert.Equal(str1, await sr.ReadLineAsync(default));
85+
}
86+
}
87+
7188
[Fact]
7289
public static void ReadPseudoRandomString()
7390
{
@@ -155,6 +172,14 @@ public static void ReadToEndPseudoRandom() {
155172
Assert.Equal(str1, sr.ReadToEnd());
156173
}
157174

175+
[Fact]
176+
public static async Task ReadToEndAsyncString()
177+
{
178+
string str1 = "Hello\0\t\v \\ World";
179+
StringReader sr = new StringReader(str1);
180+
Assert.Equal(str1, await sr.ReadToEndAsync(default));
181+
}
182+
158183
[Fact]
159184
public static void Closed_DisposedExceptions()
160185
{
@@ -278,6 +303,8 @@ public async Task Precanceled_ThrowsException()
278303

279304
await Assert.ThrowsAnyAsync<OperationCanceledException>(() => reader.ReadAsync(Memory<char>.Empty, new CancellationToken(true)).AsTask());
280305
await Assert.ThrowsAnyAsync<OperationCanceledException>(() => reader.ReadBlockAsync(Memory<char>.Empty, new CancellationToken(true)).AsTask());
306+
await Assert.ThrowsAnyAsync<OperationCanceledException>(() => reader.ReadLineAsync(new CancellationToken(true)).AsTask());
307+
await Assert.ThrowsAnyAsync<OperationCanceledException>(() => reader.ReadToEndAsync(new CancellationToken(true)));
281308
}
282309

283310
private static void ValidateDisposedExceptions(StringReader sr)

src/libraries/System.IO/tests/TextReader/TextReaderTests.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Threading;
45
using System.Threading.Tasks;
56
using Xunit;
67

@@ -54,6 +55,23 @@ public async Task ReadToEndAsync()
5455
}
5556
}
5657

58+
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
59+
public async Task ReadToEndAsync_WithCancellationToken()
60+
{
61+
using var tr = new CharArrayTextReader(TestDataProvider.LargeData);
62+
var result = await tr.ReadToEndAsync(default);
63+
Assert.Equal(5000, result.Length);
64+
}
65+
66+
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
67+
public async Task ReadToEndAsync_WithCanceledCancellationToken()
68+
{
69+
using var tr = new CharArrayTextReader(TestDataProvider.LargeData);
70+
using var cts = new CancellationTokenSource();
71+
cts.Cancel();
72+
await Assert.ThrowsAnyAsync<OperationCanceledException>(async () => await tr.ReadToEndAsync(cts.Token));
73+
}
74+
5775
[Fact]
5876
public void TestRead()
5977
{

src/libraries/System.Private.CoreLib/src/System/IO/File.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -649,7 +649,7 @@ private static async Task<string[]> InternalReadAllLinesAsync(string path, Encod
649649
cancellationToken.ThrowIfCancellationRequested();
650650
string? line;
651651
List<string> lines = new List<string>();
652-
while ((line = await sr.ReadLineAsync().ConfigureAwait(false)) != null)
652+
while ((line = await sr.ReadLineAsync(cancellationToken).ConfigureAwait(false)) != null)
653653
{
654654
lines.Add(line);
655655
cancellationToken.ThrowIfCancellationRequested();

src/libraries/System.Private.CoreLib/src/System/IO/StreamReader.cs

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -845,29 +845,32 @@ private int ReadBuffer(Span<char> userBuffer, out bool readToUserBuffer)
845845
return sb.ToString();
846846
}
847847

848-
public override Task<string?> ReadLineAsync()
848+
public override Task<string?> ReadLineAsync() =>
849+
ReadLineAsync(default).AsTask();
850+
851+
public override ValueTask<string?> ReadLineAsync(CancellationToken cancellationToken)
849852
{
850853
// If we have been inherited into a subclass, the following implementation could be incorrect
851854
// since it does not call through to Read() which a subclass might have overridden.
852855
// To be safe we will only use this implementation in cases where we know it is safe to do so,
853856
// and delegate to our base class (which will call into Read) when we are not sure.
854857
if (GetType() != typeof(StreamReader))
855858
{
856-
return base.ReadLineAsync();
859+
return base.ReadLineAsync(cancellationToken);
857860
}
858861

859862
ThrowIfDisposed();
860863
CheckAsyncTaskInProgress();
861864

862-
Task<string?> task = ReadLineAsyncInternal();
865+
Task<string?> task = ReadLineAsyncInternal(cancellationToken);
863866
_asyncReadTask = task;
864867

865-
return task;
868+
return new ValueTask<string?>(task);
866869
}
867870

868-
private async Task<string?> ReadLineAsyncInternal()
871+
private async Task<string?> ReadLineAsyncInternal(CancellationToken cancellationToken)
869872
{
870-
if (_charPos == _charLen && (await ReadBufferAsync(CancellationToken.None).ConfigureAwait(false)) == 0)
873+
if (_charPos == _charLen && (await ReadBufferAsync(cancellationToken).ConfigureAwait(false)) == 0)
871874
{
872875
return null;
873876
}
@@ -903,7 +906,7 @@ private int ReadBuffer(Span<char> userBuffer, out bool readToUserBuffer)
903906

904907
_charPos = tmpCharPos = i + 1;
905908

906-
if (ch == '\r' && (tmpCharPos < tmpCharLen || (await ReadBufferAsync(CancellationToken.None).ConfigureAwait(false)) > 0))
909+
if (ch == '\r' && (tmpCharPos < tmpCharLen || (await ReadBufferAsync(cancellationToken).ConfigureAwait(false)) > 0))
907910
{
908911
tmpCharPos = _charPos;
909912
if (_charBuffer[tmpCharPos] == '\n')
@@ -921,32 +924,35 @@ private int ReadBuffer(Span<char> userBuffer, out bool readToUserBuffer)
921924
i = tmpCharLen - tmpCharPos;
922925
sb ??= new StringBuilder(i + 80);
923926
sb.Append(tmpCharBuffer, tmpCharPos, i);
924-
} while (await ReadBufferAsync(CancellationToken.None).ConfigureAwait(false) > 0);
927+
} while (await ReadBufferAsync(cancellationToken).ConfigureAwait(false) > 0);
925928

926929
return sb.ToString();
927930
}
928931

929-
public override Task<string> ReadToEndAsync()
932+
public override Task<string> ReadToEndAsync() =>
933+
ReadToEndAsync(default);
934+
935+
public override Task<string> ReadToEndAsync(CancellationToken cancellationToken)
930936
{
931937
// If we have been inherited into a subclass, the following implementation could be incorrect
932938
// since it does not call through to Read() which a subclass might have overridden.
933939
// To be safe we will only use this implementation in cases where we know it is safe to do so,
934940
// and delegate to our base class (which will call into Read) when we are not sure.
935941
if (GetType() != typeof(StreamReader))
936942
{
937-
return base.ReadToEndAsync();
943+
return base.ReadToEndAsync(cancellationToken);
938944
}
939945

940946
ThrowIfDisposed();
941947
CheckAsyncTaskInProgress();
942948

943-
Task<string> task = ReadToEndAsyncInternal();
949+
Task<string> task = ReadToEndAsyncInternal(cancellationToken);
944950
_asyncReadTask = task;
945951

946952
return task;
947953
}
948954

949-
private async Task<string> ReadToEndAsyncInternal()
955+
private async Task<string> ReadToEndAsyncInternal(CancellationToken cancellationToken)
950956
{
951957
// Call ReadBuffer, then pull data out of charBuffer.
952958
StringBuilder sb = new StringBuilder(_charLen - _charPos);
@@ -955,7 +961,7 @@ private async Task<string> ReadToEndAsyncInternal()
955961
int tmpCharPos = _charPos;
956962
sb.Append(_charBuffer, tmpCharPos, _charLen - tmpCharPos);
957963
_charPos = _charLen; // We consumed these characters
958-
await ReadBufferAsync(CancellationToken.None).ConfigureAwait(false);
964+
await ReadBufferAsync(cancellationToken).ConfigureAwait(false);
959965
} while (_charLen > 0);
960966

961967
return sb.ToString();

src/libraries/System.Private.CoreLib/src/System/IO/StringReader.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,11 +224,21 @@ public override string ReadToEnd()
224224
return Task.FromResult(ReadLine());
225225
}
226226

227+
public override ValueTask<string?> ReadLineAsync(CancellationToken cancellationToken) =>
228+
cancellationToken.IsCancellationRequested
229+
? ValueTask.FromCanceled<string?>(cancellationToken)
230+
: new ValueTask<string?>(ReadLine());
231+
227232
public override Task<string> ReadToEndAsync()
228233
{
229234
return Task.FromResult(ReadToEnd());
230235
}
231236

237+
public override Task<string> ReadToEndAsync(CancellationToken cancellationToken) =>
238+
cancellationToken.IsCancellationRequested
239+
? Task.FromCanceled<string>(cancellationToken)
240+
: Task.FromResult(ReadToEnd());
241+
232242
public override Task<int> ReadBlockAsync(char[] buffer, int index, int count)
233243
{
234244
if (buffer == null)

src/libraries/System.Private.CoreLib/src/System/IO/TextReader.cs

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -203,18 +203,26 @@ public virtual int ReadBlock(Span<char> buffer)
203203
}
204204

205205
#region Task based Async APIs
206-
public virtual Task<string?> ReadLineAsync() =>
206+
public virtual Task<string?> ReadLineAsync() => ReadLineCoreAsync(default);
207+
208+
public virtual ValueTask<string?> ReadLineAsync(CancellationToken cancellationToken) =>
209+
new ValueTask<string?>(ReadLineCoreAsync(cancellationToken));
210+
211+
private Task<string?> ReadLineCoreAsync(CancellationToken cancellationToken) =>
207212
Task<string?>.Factory.StartNew(static state => ((TextReader)state!).ReadLine(), this,
208-
CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);
213+
cancellationToken, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);
209214

210-
public virtual async Task<string> ReadToEndAsync()
215+
public virtual Task<string> ReadToEndAsync() =>
216+
ReadToEndAsync(default);
217+
218+
public virtual async Task<string> ReadToEndAsync(CancellationToken cancellationToken)
211219
{
212220
var sb = new StringBuilder(4096);
213221
char[] chars = ArrayPool<char>.Shared.Rent(4096);
214222
try
215223
{
216224
int len;
217-
while ((len = await ReadAsyncInternal(chars, default).ConfigureAwait(false)) != 0)
225+
while ((len = await ReadAsyncInternal(chars, cancellationToken).ConfigureAwait(false)) != 0)
218226
{
219227
sb.Append(chars, 0, len);
220228
}
@@ -368,9 +376,15 @@ protected override void Dispose(bool disposing)
368376
[MethodImpl(MethodImplOptions.Synchronized)]
369377
public override Task<string?> ReadLineAsync() => Task.FromResult(ReadLine());
370378

379+
[MethodImpl(MethodImplOptions.Synchronized)]
380+
public override ValueTask<string?> ReadLineAsync(CancellationToken cancellationToken) => cancellationToken.IsCancellationRequested ? ValueTask.FromCanceled<string?>(cancellationToken) : new ValueTask<string?>(ReadLine());
381+
371382
[MethodImpl(MethodImplOptions.Synchronized)]
372383
public override Task<string> ReadToEndAsync() => Task.FromResult(ReadToEnd());
373384

385+
[MethodImpl(MethodImplOptions.Synchronized)]
386+
public override Task<string> ReadToEndAsync(CancellationToken cancellationToken) => cancellationToken.IsCancellationRequested ? Task.FromCanceled<string>(cancellationToken) : Task.FromResult(ReadToEnd());
387+
374388
[MethodImpl(MethodImplOptions.Synchronized)]
375389
public override Task<int> ReadBlockAsync(char[] buffer, int index, int count)
376390
{

0 commit comments

Comments
 (0)