diff --git a/src/libraries/System.Console/src/System/IO/SyncTextReader.cs b/src/libraries/System.Console/src/System/IO/SyncTextReader.cs index 7a2661691e624..33ef898edb688 100644 --- a/src/libraries/System.Console/src/System/IO/SyncTextReader.cs +++ b/src/libraries/System.Console/src/System/IO/SyncTextReader.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.Runtime.InteropServices; +using System.Threading; using System.Threading.Tasks; namespace System.IO @@ -95,11 +96,25 @@ public override string ReadToEnd() return Task.FromResult(ReadLine()); } + public override ValueTask ReadLineAsync(CancellationToken cancellationToken) + { + return cancellationToken.IsCancellationRequested ? + ValueTask.FromCanceled(cancellationToken) : + new ValueTask(ReadLine()); + } + public override Task ReadToEndAsync() { return Task.FromResult(ReadToEnd()); } + public override Task ReadToEndAsync(CancellationToken cancellationToken) + { + return cancellationToken.IsCancellationRequested ? + Task.FromCanceled(cancellationToken) : + Task.FromResult(ReadToEnd()); + } + public override Task ReadBlockAsync(char[] buffer, int index, int count) { if (buffer == null) diff --git a/src/libraries/System.IO/tests/StreamReader/StreamReaderTests.cs b/src/libraries/System.IO/tests/StreamReader/StreamReaderTests.cs index 4a2b91f8862b5..fccc269868ed7 100644 --- a/src/libraries/System.IO/tests/StreamReader/StreamReaderTests.cs +++ b/src/libraries/System.IO/tests/StreamReader/StreamReaderTests.cs @@ -110,6 +110,44 @@ public async Task ReadToEndAsync() Assert.Equal(5000, result.Length); } + [Fact] + public async Task ReadToEndAsync_WithCancellationToken() + { + using var sw = new StreamReader(GetLargeStream()); + var result = await sw.ReadToEndAsync(default); + + Assert.Equal(5000, result.Length); + } + + [Fact] + public async Task ReadToEndAsync_WithCanceledCancellationToken() + { + using var sw = new StreamReader(GetLargeStream()); + using var cts = new CancellationTokenSource(); + cts.Cancel(); + var token = cts.Token; + + var ex = await Assert.ThrowsAnyAsync(async () => await sw.ReadToEndAsync(token)); + Assert.Equal(token, ex.CancellationToken); + } + + [Fact] + [SkipOnPlatform(TestPlatforms.Browser, "Not supported on Browser.")] + public async Task ReadToEndAsync_WithCancellation() + { + string path = GetTestFilePath(); + + // create large (~100MB) file + File.WriteAllLines(path, Enumerable.Repeat("A very large file used for testing StreamReader cancellation. 0123456789012345678901234567890123456789.", 1_000_000)); + + using StreamReader reader = File.OpenText(path); + using CancellationTokenSource cts = new (TimeSpan.FromMilliseconds(50)); + var token = cts.Token; + + var ex = await Assert.ThrowsAnyAsync(async () => await reader.ReadToEndAsync(token)); + Assert.Equal(token, ex.CancellationToken); + } + [Fact] public void GetBaseStream() { @@ -301,6 +339,27 @@ public void VanillaReadLines2() Assert.Equal(valueString.Substring(1, valueString.IndexOf('\r') - 1), data); } + [Fact] + public async Task VanillaReadLineAsync() + { + var baseInfo = GetCharArrayStream(); + var sr = baseInfo.Item2; + + string valueString = new string(baseInfo.Item1); + + var data = await sr.ReadLineAsync(); + Assert.Equal(valueString.Substring(0, valueString.IndexOf('\r')), data); + + data = await sr.ReadLineAsync(default); + Assert.Equal(valueString.Substring(valueString.IndexOf('\r') + 1, 3), data); + + data = await sr.ReadLineAsync(); + Assert.Equal(valueString.Substring(valueString.IndexOf('\n') + 1, 2), data); + + data = await sr.ReadLineAsync(default); + Assert.Equal((valueString.Substring(valueString.LastIndexOf('\n') + 1)), data); + } + [Fact] public async Task ContinuousNewLinesAndTabsAsync() { diff --git a/src/libraries/System.IO/tests/StringReader/StringReader.CtorTests.cs b/src/libraries/System.IO/tests/StringReader/StringReader.CtorTests.cs index 9e500923164f3..3c4446f07f47a 100644 --- a/src/libraries/System.IO/tests/StringReader/StringReader.CtorTests.cs +++ b/src/libraries/System.IO/tests/StringReader/StringReader.CtorTests.cs @@ -68,6 +68,23 @@ public static void ReadLine() } } + [Fact] + public static async Task ReadLineAsync() + { + string str1 = "Hello\0\t\v \\ World"; + string str2 = str1 + Environment.NewLine + str1; + + using (StringReader sr = new StringReader(str1)) + { + Assert.Equal(str1, await sr.ReadLineAsync()); + } + using (StringReader sr = new StringReader(str2)) + { + Assert.Equal(str1, await sr.ReadLineAsync(default)); + Assert.Equal(str1, await sr.ReadLineAsync(default)); + } + } + [Fact] public static void ReadPseudoRandomString() { @@ -155,6 +172,14 @@ public static void ReadToEndPseudoRandom() { Assert.Equal(str1, sr.ReadToEnd()); } + [Fact] + public static async Task ReadToEndAsyncString() + { + string str1 = "Hello\0\t\v \\ World"; + StringReader sr = new StringReader(str1); + Assert.Equal(str1, await sr.ReadToEndAsync(default)); + } + [Fact] public static void Closed_DisposedExceptions() { @@ -278,6 +303,8 @@ public async Task Precanceled_ThrowsException() await Assert.ThrowsAnyAsync(() => reader.ReadAsync(Memory.Empty, new CancellationToken(true)).AsTask()); await Assert.ThrowsAnyAsync(() => reader.ReadBlockAsync(Memory.Empty, new CancellationToken(true)).AsTask()); + await Assert.ThrowsAnyAsync(() => reader.ReadLineAsync(new CancellationToken(true)).AsTask()); + await Assert.ThrowsAnyAsync(() => reader.ReadToEndAsync(new CancellationToken(true))); } private static void ValidateDisposedExceptions(StringReader sr) diff --git a/src/libraries/System.IO/tests/TextReader/TextReaderTests.cs b/src/libraries/System.IO/tests/TextReader/TextReaderTests.cs index e6ba6d818f286..48f83d7108e53 100644 --- a/src/libraries/System.IO/tests/TextReader/TextReaderTests.cs +++ b/src/libraries/System.IO/tests/TextReader/TextReaderTests.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Threading; using System.Threading.Tasks; using Xunit; @@ -54,6 +55,26 @@ public async Task ReadToEndAsync() } } + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))] + public async Task ReadToEndAsync_WithCancellationToken() + { + using var tr = new CharArrayTextReader(TestDataProvider.LargeData); + var result = await tr.ReadToEndAsync(default); + Assert.Equal(5000, result.Length); + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))] + public async Task ReadToEndAsync_WithCanceledCancellationToken() + { + using var tr = new CharArrayTextReader(TestDataProvider.LargeData); + using var cts = new CancellationTokenSource(); + cts.Cancel(); + var token = cts.Token; + + var ex = await Assert.ThrowsAnyAsync(async () => await tr.ReadToEndAsync(token)); + Assert.Equal(token, ex.CancellationToken); + } + [Fact] public void TestRead() { diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/File.cs b/src/libraries/System.Private.CoreLib/src/System/IO/File.cs index 2b4c61f1eb0ba..007a16d590b0a 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/File.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/File.cs @@ -649,7 +649,7 @@ private static async Task InternalReadAllLinesAsync(string path, Encod cancellationToken.ThrowIfCancellationRequested(); string? line; List lines = new List(); - while ((line = await sr.ReadLineAsync().ConfigureAwait(false)) != null) + while ((line = await sr.ReadLineAsync(cancellationToken).ConfigureAwait(false)) != null) { lines.Add(line); cancellationToken.ThrowIfCancellationRequested(); diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/StreamReader.cs b/src/libraries/System.Private.CoreLib/src/System/IO/StreamReader.cs index 3b3e03a806ff5..b5aca3d1dfbcf 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/StreamReader.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/StreamReader.cs @@ -845,7 +845,37 @@ private int ReadBuffer(Span userBuffer, out bool readToUserBuffer) return sb.ToString(); } - public override Task ReadLineAsync() + public override Task ReadLineAsync() => + ReadLineAsync(default).AsTask(); + + /// + /// Reads a line of characters asynchronously from the current stream and returns the data as a string. + /// + /// The token to monitor for cancellation requests. + /// A value task that represents the asynchronous read operation. The value of the TResult + /// parameter contains the next line from the stream, or is if all of the characters have been read. + /// The number of characters in the next line is larger than . + /// The stream reader has been disposed. + /// The reader is currently in use by a previous read operation. + /// + /// The following example shows how to read and print all lines from the file until the end of the file is reached or the operation timed out. + /// + /// using CancellationTokenSource tokenSource = new (TimeSpan.FromSeconds(1)); + /// using StreamReader reader = File.OpenText("existingfile.txt"); + /// + /// string line; + /// while ((line = await reader.ReadLineAsync(tokenSource.Token)) is not null) + /// { + /// Console.WriteLine(line); + /// } + /// + /// + /// + /// If this method is canceled via , some data + /// that has been read from the current but not stored (by the + /// ) or returned (to the caller) may be lost. + /// + public override ValueTask ReadLineAsync(CancellationToken cancellationToken) { // If we have been inherited into a subclass, the following implementation could be incorrect // since it does not call through to Read() which a subclass might have overridden. @@ -853,21 +883,21 @@ private int ReadBuffer(Span userBuffer, out bool readToUserBuffer) // and delegate to our base class (which will call into Read) when we are not sure. if (GetType() != typeof(StreamReader)) { - return base.ReadLineAsync(); + return base.ReadLineAsync(cancellationToken); } ThrowIfDisposed(); CheckAsyncTaskInProgress(); - Task task = ReadLineAsyncInternal(); + Task task = ReadLineAsyncInternal(cancellationToken); _asyncReadTask = task; - return task; + return new ValueTask(task); } - private async Task ReadLineAsyncInternal() + private async Task ReadLineAsyncInternal(CancellationToken cancellationToken) { - if (_charPos == _charLen && (await ReadBufferAsync(CancellationToken.None).ConfigureAwait(false)) == 0) + if (_charPos == _charLen && (await ReadBufferAsync(cancellationToken).ConfigureAwait(false)) == 0) { return null; } @@ -903,7 +933,7 @@ private int ReadBuffer(Span userBuffer, out bool readToUserBuffer) _charPos = tmpCharPos = i + 1; - if (ch == '\r' && (tmpCharPos < tmpCharLen || (await ReadBufferAsync(CancellationToken.None).ConfigureAwait(false)) > 0)) + if (ch == '\r' && (tmpCharPos < tmpCharLen || (await ReadBufferAsync(cancellationToken).ConfigureAwait(false)) > 0)) { tmpCharPos = _charPos; if (_charBuffer[tmpCharPos] == '\n') @@ -921,12 +951,37 @@ private int ReadBuffer(Span userBuffer, out bool readToUserBuffer) i = tmpCharLen - tmpCharPos; sb ??= new StringBuilder(i + 80); sb.Append(tmpCharBuffer, tmpCharPos, i); - } while (await ReadBufferAsync(CancellationToken.None).ConfigureAwait(false) > 0); + } while (await ReadBufferAsync(cancellationToken).ConfigureAwait(false) > 0); return sb.ToString(); } - public override Task ReadToEndAsync() + public override Task ReadToEndAsync() => ReadToEndAsync(default); + + /// + /// Reads all characters from the current position to the end of the stream asynchronously and returns them as one string. + /// + /// The token to monitor for cancellation requests. + /// A task that represents the asynchronous read operation. The value of the TResult parameter contains + /// a string with the characters from the current position to the end of the stream. + /// The number of characters is larger than . + /// The stream reader has been disposed. + /// The reader is currently in use by a previous read operation. + /// + /// The following example shows how to read the contents of a file by using the method. + /// + /// using CancellationTokenSource tokenSource = new (TimeSpan.FromSeconds(1)); + /// using StreamReader reader = File.OpenText("existingfile.txt"); + /// + /// Console.WriteLine(await reader.ReadToEndAsync(tokenSource.Token)); + /// + /// + /// + /// If this method is canceled via , some data + /// that has been read from the current but not stored (by the + /// ) or returned (to the caller) may be lost. + /// + public override Task ReadToEndAsync(CancellationToken cancellationToken) { // If we have been inherited into a subclass, the following implementation could be incorrect // since it does not call through to Read() which a subclass might have overridden. @@ -934,19 +989,19 @@ public override Task ReadToEndAsync() // and delegate to our base class (which will call into Read) when we are not sure. if (GetType() != typeof(StreamReader)) { - return base.ReadToEndAsync(); + return base.ReadToEndAsync(cancellationToken); } ThrowIfDisposed(); CheckAsyncTaskInProgress(); - Task task = ReadToEndAsyncInternal(); + Task task = ReadToEndAsyncInternal(cancellationToken); _asyncReadTask = task; return task; } - private async Task ReadToEndAsyncInternal() + private async Task ReadToEndAsyncInternal(CancellationToken cancellationToken) { // Call ReadBuffer, then pull data out of charBuffer. StringBuilder sb = new StringBuilder(_charLen - _charPos); @@ -955,7 +1010,7 @@ private async Task ReadToEndAsyncInternal() int tmpCharPos = _charPos; sb.Append(_charBuffer, tmpCharPos, _charLen - tmpCharPos); _charPos = _charLen; // We consumed these characters - await ReadBufferAsync(CancellationToken.None).ConfigureAwait(false); + await ReadBufferAsync(cancellationToken).ConfigureAwait(false); } while (_charLen > 0); return sb.ToString(); diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/StringReader.cs b/src/libraries/System.Private.CoreLib/src/System/IO/StringReader.cs index 3d50897ec7f38..a8e07ad382d15 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/StringReader.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/StringReader.cs @@ -224,11 +224,74 @@ public override string ReadToEnd() return Task.FromResult(ReadLine()); } + /// + /// Reads a line of characters asynchronously from the current string and returns the data as a string. + /// + /// The token to monitor for cancellation requests. + /// A value task that represents the asynchronous read operation. The value of the TResult + /// parameter contains the next line from the string reader, or is if all of the characters have been read. + /// The number of characters in the next line is larger than . + /// The string reader has been disposed. + /// The reader is currently in use by a previous read operation. + /// + /// The following example shows how to read one line at a time from a string asynchronously. + /// + /// using System.Text; + /// + /// StringBuilder stringToRead = new(); + /// stringToRead.AppendLine("Characters in 1st line to read"); + /// stringToRead.AppendLine("and 2nd line"); + /// stringToRead.AppendLine("and the end"); + /// + /// string readText; + /// using CancellationTokenSource tokenSource = new (TimeSpan.FromSeconds(1)); + /// using StringReader reader = new (stringToRead.ToString()); + /// while ((readText = await reader.ReadLineAsync(tokenSource.Token)) is not null) + /// { + /// Console.WriteLine(readText); + /// } + /// + /// + public override ValueTask ReadLineAsync(CancellationToken cancellationToken) => + cancellationToken.IsCancellationRequested + ? ValueTask.FromCanceled(cancellationToken) + : new ValueTask(ReadLine()); + public override Task ReadToEndAsync() { return Task.FromResult(ReadToEnd()); } + /// + /// Reads all characters from the current position to the end of the string asynchronously and returns them as a single string. + /// + /// The token to monitor for cancellation requests. + /// A task that represents the asynchronous read operation. The value of the TResult parameter contains + /// a string with the characters from the current position to the end of the string. + /// The number of characters is larger than . + /// The string reader has been disposed. + /// The reader is currently in use by a previous read operation. + /// + /// The following example shows how to read an entire string asynchronously. + /// + /// using System.Text; + /// + /// StringBuilder stringToRead = new(); + /// stringToRead.AppendLine("Characters in 1st line to read"); + /// stringToRead.AppendLine("and 2nd line"); + /// stringToRead.AppendLine("and the end"); + /// + /// using CancellationTokenSource tokenSource = new (TimeSpan.FromSeconds(1)); + /// using StringReader reader = new (stringToRead.ToString()); + /// var readText = await reader.ReadToEndAsync(tokenSource.Token); + /// Console.WriteLine(readText); + /// + /// + public override Task ReadToEndAsync(CancellationToken cancellationToken) => + cancellationToken.IsCancellationRequested + ? Task.FromCanceled(cancellationToken) + : Task.FromResult(ReadToEnd()); + public override Task ReadBlockAsync(char[] buffer, int index, int count) { if (buffer == null) diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/TextReader.cs b/src/libraries/System.Private.CoreLib/src/System/IO/TextReader.cs index 05211d6dc7a33..583fb3c2652ae 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/TextReader.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/TextReader.cs @@ -203,18 +203,56 @@ public virtual int ReadBlock(Span buffer) } #region Task based Async APIs - public virtual Task ReadLineAsync() => + public virtual Task ReadLineAsync() => ReadLineCoreAsync(default); + + /// + /// Reads a line of characters asynchronously and returns the data as a string. + /// + /// The token to monitor for cancellation requests. + /// A value task that represents the asynchronous read operation. The value of the TResult + /// parameter contains the next line from the text reader, or is if all of the characters have been read. + /// The number of characters in the next line is larger than . + /// The text reader has been disposed. + /// The reader is currently in use by a previous read operation. + /// + /// The class is an abstract class. Therefore, you do not instantiate it in + /// your code. For an example of using the method, see the + /// method. + /// If the current represents the standard input stream returned by + /// the Console.In property, the method + /// executes synchronously rather than asynchronously. + /// + public virtual ValueTask ReadLineAsync(CancellationToken cancellationToken) => + new ValueTask(ReadLineCoreAsync(cancellationToken)); + + private Task ReadLineCoreAsync(CancellationToken cancellationToken) => Task.Factory.StartNew(static state => ((TextReader)state!).ReadLine(), this, - CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); - - public virtual async Task ReadToEndAsync() + cancellationToken, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); + + public virtual Task ReadToEndAsync() => ReadToEndAsync(default); + + /// + /// Reads all characters from the current position to the end of the text reader asynchronously and returns them as one string. + /// + /// The token to monitor for cancellation requests. + /// A task that represents the asynchronous read operation. The value of the TResult parameter contains + /// a string with the characters from the current position to the end of the text reader. + /// The number of characters is larger than . + /// The text reader has been disposed. + /// The reader is currently in use by a previous read operation. + /// + /// The class is an abstract class. Therefore, you do not instantiate it in + /// your code. For an example of using the method, see the + /// method. + /// + public virtual async Task ReadToEndAsync(CancellationToken cancellationToken) { var sb = new StringBuilder(4096); char[] chars = ArrayPool.Shared.Rent(4096); try { int len; - while ((len = await ReadAsyncInternal(chars, default).ConfigureAwait(false)) != 0) + while ((len = await ReadAsyncInternal(chars, cancellationToken).ConfigureAwait(false)) != 0) { sb.Append(chars, 0, len); } @@ -368,9 +406,17 @@ protected override void Dispose(bool disposing) [MethodImpl(MethodImplOptions.Synchronized)] public override Task ReadLineAsync() => Task.FromResult(ReadLine()); + [MethodImpl(MethodImplOptions.Synchronized)] + public override ValueTask ReadLineAsync(CancellationToken cancellationToken) + => cancellationToken.IsCancellationRequested ? ValueTask.FromCanceled(cancellationToken) : new ValueTask(ReadLine()); + [MethodImpl(MethodImplOptions.Synchronized)] public override Task ReadToEndAsync() => Task.FromResult(ReadToEnd()); + [MethodImpl(MethodImplOptions.Synchronized)] + public override Task ReadToEndAsync(CancellationToken cancellationToken) + => cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) : Task.FromResult(ReadToEnd()); + [MethodImpl(MethodImplOptions.Synchronized)] public override Task ReadBlockAsync(char[] buffer, int index, int count) { diff --git a/src/libraries/System.Runtime/ref/System.Runtime.cs b/src/libraries/System.Runtime/ref/System.Runtime.cs index 27cbc4c98d261..41627e49ef8f1 100644 --- a/src/libraries/System.Runtime/ref/System.Runtime.cs +++ b/src/libraries/System.Runtime/ref/System.Runtime.cs @@ -10855,8 +10855,10 @@ protected override void Dispose(bool disposing) { } public override System.Threading.Tasks.ValueTask ReadBlockAsync(System.Memory buffer, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public override string? ReadLine() { throw null; } public override System.Threading.Tasks.Task ReadLineAsync() { throw null; } + public override System.Threading.Tasks.ValueTask ReadLineAsync(System.Threading.CancellationToken cancellationToken) { throw null; } public override string ReadToEnd() { throw null; } public override System.Threading.Tasks.Task ReadToEndAsync() { throw null; } + public override System.Threading.Tasks.Task ReadToEndAsync(System.Threading.CancellationToken cancellationToken) { throw null; } } public partial class StreamWriter : System.IO.TextWriter { @@ -10920,8 +10922,10 @@ protected override void Dispose(bool disposing) { } public override System.Threading.Tasks.ValueTask ReadBlockAsync(System.Memory buffer, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public override string? ReadLine() { throw null; } public override System.Threading.Tasks.Task ReadLineAsync() { throw null; } + public override System.Threading.Tasks.ValueTask ReadLineAsync(System.Threading.CancellationToken cancellationToken) { throw null; } public override string ReadToEnd() { throw null; } public override System.Threading.Tasks.Task ReadToEndAsync() { throw null; } + public override System.Threading.Tasks.Task ReadToEndAsync(System.Threading.CancellationToken cancellationToken) { throw null; } } public partial class StringWriter : System.IO.TextWriter { @@ -10972,8 +10976,10 @@ protected virtual void Dispose(bool disposing) { } public virtual System.Threading.Tasks.ValueTask ReadBlockAsync(System.Memory buffer, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public virtual string? ReadLine() { throw null; } public virtual System.Threading.Tasks.Task ReadLineAsync() { throw null; } + public virtual System.Threading.Tasks.ValueTask ReadLineAsync(System.Threading.CancellationToken cancellationToken) { throw null; } public virtual string ReadToEnd() { throw null; } public virtual System.Threading.Tasks.Task ReadToEndAsync() { throw null; } + public virtual System.Threading.Tasks.Task ReadToEndAsync(System.Threading.CancellationToken cancellationToken) { throw null; } public static System.IO.TextReader Synchronized(System.IO.TextReader reader) { throw null; } } public abstract partial class TextWriter : System.MarshalByRefObject, System.IAsyncDisposable, System.IDisposable