diff --git a/src/libraries/System.IO/tests/Stream/Stream.NullTests.cs b/src/libraries/System.IO/tests/Stream/Stream.NullTests.cs index efe700c1e9c753..e6a5c50b830bcc 100644 --- a/src/libraries/System.IO/tests/Stream/Stream.NullTests.cs +++ b/src/libraries/System.IO/tests/Stream/Stream.NullTests.cs @@ -33,6 +33,13 @@ public static void TestNullStream_Dispose() Stream.Null.Dispose(); // Dispose shouldn't have any side effects } + [Fact] + public static async Task TestNullStream_DisposeAsync() + { + await Stream.Null.DisposeAsync(); + await Stream.Null.DisposeAsync(); // Dispose shouldn't have any side effects + } + [Fact] public static async Task TestNullStream_CopyTo() { @@ -133,6 +140,16 @@ public static void TestNullTextReaderDispose(TextReader input) Assert.Equal("", input.ReadToEnd()); } + [Theory] + [MemberData(nameof(NullReaders))] + public static async Task TestNullTextReaderDisposeAsync(TextReader input) + { + // dispose should be a no-op + await input.DisposeAsync(); + await input.DisposeAsync(); + Assert.Equal("", input.ReadToEnd()); + } + [Theory] [MemberData(nameof(NullReaders))] public static void TestNullTextReader(TextReader input) @@ -172,6 +189,22 @@ public static async Task TestNullTextReaderAsync(TextReader input) input.Dispose(); } + [Theory] + [MemberData(nameof(NullReaders))] + public static async Task TestNullTextReaderAsync_DisposeAsync(TextReader input) + { + var chars = new char[2]; + Assert.Equal(0, await input.ReadAsync(chars, 0, chars.Length)); + Assert.Equal(0, await input.ReadAsync(chars.AsMemory(), default)); + Assert.Equal(0, await input.ReadBlockAsync(chars, 0, chars.Length)); + Assert.Equal(0, await input.ReadBlockAsync(chars.AsMemory(), default)); + Assert.Null(await input.ReadLineAsync()); + Assert.Null(await input.ReadLineAsync(default)); + Assert.Equal("", await input.ReadToEndAsync()); + Assert.Equal("", await input.ReadToEndAsync(default)); + await input.DisposeAsync(); + } + [Theory] [MemberData(nameof(NullReaders))] public static async Task TestCanceledNullTextReaderAsync(TextReader input) @@ -192,6 +225,76 @@ public static async Task TestCanceledNullTextReaderAsync(TextReader input) input.Dispose(); } + [Theory] + [MemberData(nameof(NullReaders))] + public static async Task TestCanceledNullTextReaderAsync_DisposeAsync(TextReader input) + { + using CancellationTokenSource tokenSource = new CancellationTokenSource(); + tokenSource.Cancel(); + var token = tokenSource.Token; + var chars = new char[2]; + OperationCanceledException ex; + ex = await Assert.ThrowsAnyAsync(async () => await input.ReadAsync(chars.AsMemory(), token)); + Assert.Equal(token, ex.CancellationToken); + ex = await Assert.ThrowsAnyAsync(async () => await input.ReadBlockAsync(chars.AsMemory(), token)); + Assert.Equal(token, ex.CancellationToken); + ex = await Assert.ThrowsAnyAsync(async () => await input.ReadLineAsync(token)); + Assert.Equal(token, ex.CancellationToken); + ex = await Assert.ThrowsAnyAsync(async () => await input.ReadToEndAsync(token)); + Assert.Equal(token, ex.CancellationToken); + await input.DisposeAsync(); + } + + [Theory] + [MemberData(nameof(NullReaders))] + public static void TestNullTextReader_Disposed(TextReader input) + { + StreamReader sr = input as StreamReader; + + input.Dispose(); + + if (sr != null) + Assert.True(sr.EndOfStream, "EndOfStream property didn't return true"); + Assert.Null(input.ReadLine()); + if (sr != null) + Assert.True(sr.EndOfStream, "EndOfStream property didn't return true"); + + Assert.Equal(-1, input.Read()); + Assert.Equal(-1, input.Peek()); + var chars = new char[2]; + Assert.Equal(0, input.Read(chars, 0, chars.Length)); + Assert.Equal(0, input.Read(chars.AsSpan())); + Assert.Equal(0, input.ReadBlock(chars, 0, chars.Length)); + Assert.Equal(0, input.ReadBlock(chars.AsSpan())); + Assert.Equal("", input.ReadToEnd()); + input.Dispose(); + } + + [Theory] + [MemberData(nameof(NullReaders))] + public static async Task TestNullTextReader_DisposedAsync(TextReader input) + { + StreamReader sr = input as StreamReader; + + await input.DisposeAsync(); + + if (sr != null) + Assert.True(sr.EndOfStream, "EndOfStream property didn't return true"); + Assert.Null(input.ReadLine()); + if (sr != null) + Assert.True(sr.EndOfStream, "EndOfStream property didn't return true"); + + Assert.Equal(-1, input.Read()); + Assert.Equal(-1, input.Peek()); + var chars = new char[2]; + Assert.Equal(0, await input.ReadAsync(chars, 0, chars.Length)); + Assert.Equal(0, input.Read(chars.AsSpan())); + Assert.Equal(0, await input.ReadBlockAsync(chars, 0, chars.Length)); + Assert.Equal(0, input.ReadBlock(chars.AsSpan())); + Assert.Equal("", await input.ReadToEndAsync()); + await input.DisposeAsync(); + } + [Theory] [MemberData(nameof(NullWriters))] public static void TextNullTextWriter(TextWriter output) @@ -206,6 +309,20 @@ public static void TextNullTextWriter(TextWriter output) output.Dispose(); } + [Theory] + [MemberData(nameof(NullWriters))] + public static async Task TextNullTextWriterAsync(TextWriter output) + { + await output.FlushAsync(); + await output.DisposeAsync(); + + await output.WriteLineAsync(decimal.MinValue.ToString()); + await output.WriteLineAsync(Math.PI.ToString()); + await output.WriteLineAsync(output.NewLine); + await output.FlushAsync(); + await output.DisposeAsync(); + } + [Theory] [MemberData(nameof(NullStream_ReadWriteData))] public void TestNullStream_ReadSpan(byte[] buffer, int offset, int count) diff --git a/src/libraries/System.IO/tests/StreamReader/StreamReader.DisposeAsync.cs b/src/libraries/System.IO/tests/StreamReader/StreamReader.DisposeAsync.cs new file mode 100644 index 00000000000000..2a4a6490ddb42d --- /dev/null +++ b/src/libraries/System.IO/tests/StreamReader/StreamReader.DisposeAsync.cs @@ -0,0 +1,77 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace System.IO.Tests +{ + public class StreamReader_disposeAsyncTest + { + + [Fact] + public void DisposeAsync_CanInvokeMultipleTimes() + { + var ms = new MemoryStream(); + var sr = new StreamReader(ms); + Assert.True(sr.DisposeAsync().IsCompletedSuccessfully); + Assert.True(sr.DisposeAsync().IsCompletedSuccessfully); + } + + [Fact] + public void DisposeAsync_CanDisposeAsyncAfterDispose() + { + var ms = new MemoryStream(); + var sr = new StreamReader(ms); + sr.Dispose(); + Assert.True(sr.DisposeAsync().IsCompletedSuccessfully); + } + + [Fact] + public async Task DisposeAsync_LeaveOpenTrue_LeftOpen() + { + var ms = new MemoryStream(); + var sr = new StreamReader(ms, Encoding.ASCII, false, 0x1000, leaveOpen: true); + await sr.DisposeAsync(); + Assert.Equal(0, ms.Position); // doesn't throw + } + + [Fact] + public async Task DisposeAsync_DerivedTypeForcesDisposeToBeUsedUnlessOverridden() + { + var ms = new MemoryStream(); + var sr = new OverrideDisposeStreamReader(ms); + Assert.False(sr.DisposeInvoked); + await sr.DisposeAsync(); + Assert.True(sr.DisposeInvoked); + } + + [Fact] + public async Task DisposeAsync_DerivedTypeDisposeAsyncInvoked() + { + var ms = new MemoryStream(); + var sr = new OverrideDisposeAndDisposeAsyncStreamReader(ms); + Assert.False(sr.DisposeInvoked); + Assert.False(sr.DisposeAsyncInvoked); + await sr.DisposeAsync(); + Assert.False(sr.DisposeInvoked); + Assert.True(sr.DisposeAsyncInvoked); + } + + private sealed class OverrideDisposeStreamReader : StreamReader + { + public bool DisposeInvoked; + public OverrideDisposeStreamReader(Stream output) : base(output) { } + protected override void Dispose(bool disposing) => DisposeInvoked = true; + } + + private sealed class OverrideDisposeAndDisposeAsyncStreamReader : StreamReader + { + public bool DisposeInvoked, DisposeAsyncInvoked; + public OverrideDisposeAndDisposeAsyncStreamReader(Stream output) : base(output) { } + protected override void Dispose(bool disposing) => DisposeInvoked = true; + public override ValueTask DisposeAsync() { DisposeAsyncInvoked = true; return default; } + } + } +} diff --git a/src/libraries/System.IO/tests/System.IO.Tests.csproj b/src/libraries/System.IO/tests/System.IO.Tests.csproj index c7286f40f1518e..8c098e8f806989 100644 --- a/src/libraries/System.IO/tests/System.IO.Tests.csproj +++ b/src/libraries/System.IO/tests/System.IO.Tests.csproj @@ -29,6 +29,7 @@ + diff --git a/src/libraries/System.IO/tests/TextReader/TextReaderTests.cs b/src/libraries/System.IO/tests/TextReader/TextReaderTests.cs index d18e477153d6d7..21a4ff7f135e75 100644 --- a/src/libraries/System.IO/tests/TextReader/TextReaderTests.cs +++ b/src/libraries/System.IO/tests/TextReader/TextReaderTests.cs @@ -310,5 +310,31 @@ public void ReadBlockSpan() } } } + + [Fact] + public void DisposeAsync_InvokesDisposeSynchronously() + { + bool disposeInvoked = false; + var tw = new InvokeActionOnDisposeTextReader() { DisposeAction = () => disposeInvoked = true }; + Assert.False(disposeInvoked); + Assert.True(tw.DisposeAsync().IsCompletedSuccessfully); + Assert.True(disposeInvoked); + } + + [Fact] + public void DisposeAsync_ExceptionReturnedInTask() + { + Exception e = new FormatException(); + var tw = new InvokeActionOnDisposeTextReader() { DisposeAction = () => { throw e; } }; + ValueTask vt = tw.DisposeAsync(); + Assert.True(vt.IsFaulted); + Assert.Same(e, vt.AsTask().Exception.InnerException); + } + + private sealed class InvokeActionOnDisposeTextReader : TextReader + { + public Action DisposeAction; + protected override void Dispose(bool disposing) => DisposeAction?.Invoke(); + } } } 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 a7e5befbb4c25d..9c294da1668253 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/StreamReader.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/StreamReader.cs @@ -245,15 +245,30 @@ protected override void Dispose(bool disposing) { return; } - _disposed = true; + try + { + if (disposing) + { + CheckAsyncTaskInProgress(); + } + } + finally + { + CloseStreamFromDispose(disposing); + } + } - // Dispose of our resources if this StreamReader is closable. - if (_closable) + private void CloseStreamFromDispose(bool disposing) + { + // Dispose of our resources if this StreamWriter is closable. + if (_closable && !_disposed) { try { - // Note that Stream.Close() can potentially throw here. So we need to - // ensure cleaning up internal resources, inside the finally block. + // Attempt to close the stream even if there was an IO error from Flushing. + // Note that Stream.Close() can potentially throw here (may or may not be + // due to the same Flush error). In this case, we still need to ensure + // cleaning up internal resources, hence the finally block. if (disposing) { _stream.Close(); @@ -261,6 +276,7 @@ protected override void Dispose(bool disposing) } finally { + _disposed = true; _charPos = 0; _charLen = 0; base.Dispose(disposing); @@ -268,6 +284,51 @@ protected override void Dispose(bool disposing) } } + public override ValueTask DisposeAsync() => + GetType() != typeof(StreamReader) ? + base.DisposeAsync() : + DisposeAsyncCore(); + + private async ValueTask DisposeAsyncCore() + { + // Same logic as in StreamWriter.DisposeAsync(). + Debug.Assert(GetType() == typeof(StreamReader)); + try + { + if (!_disposed) + { + CheckAsyncTaskInProgress(); + } + } + finally + { + await CloseStreamFromDisposeAsync().ConfigureAwait(false); + } + GC.SuppressFinalize(this); + } + private async ValueTask CloseStreamFromDisposeAsync() + { + // Dispose of our resources if this StreamWriter is closable. + if (_closable && !_disposed) + { + try + { + // Attempt to close the stream even if there was an IO error from Flushing. + // Note that Stream.DisposeAsync() can potentially throw here (may or may not be + // due to the same Flush error). In this case, we still need to ensure + // cleaning up internal resources, hence the finally block. + await _stream.DisposeAsync().ConfigureAwait(false); + } + finally + { + _disposed = true; + _charPos = 0; + _charLen = 0; + await base.DisposeAsync().ConfigureAwait(false); + } + } + } + public virtual Encoding CurrentEncoding => _encoding; public virtual Stream BaseStream => _stream; diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/StreamWriter.cs b/src/libraries/System.Private.CoreLib/src/System/IO/StreamWriter.cs index df0c9e95cc6c05..1273d41f7539f1 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/StreamWriter.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/StreamWriter.cs @@ -189,13 +189,17 @@ public override void Close() protected override void Dispose(bool disposing) { + if (_disposed) + { + return; + } try { // We need to flush any buffered data if we are being closed/disposed. // Also, we never close the handles for stdout & friends. So we can safely // write any buffered data to those streams even during finalization, which // is generally the right thing to do. - if (!_disposed && disposing) + if (disposing) { // Note: flush on the underlying stream can throw (ex., low disk space) CheckAsyncTaskInProgress(); @@ -251,11 +255,33 @@ private async ValueTask DisposeAsyncCore() } finally { - CloseStreamFromDispose(disposing: true); + await CloseStreamFromDisposeAsync().ConfigureAwait(false); } GC.SuppressFinalize(this); } + private async ValueTask CloseStreamFromDisposeAsync() + { + // Dispose of our resources if this StreamWriter is closable. + if (_closable && !_disposed) + { + try + { + // Attempt to close the stream even if there was an IO error from Flushing. + // Note that Stream.DisposeAsync() can potentially throw here (may or may not be + // due to the same Flush error). In this case, we still need to ensure + // cleaning up internal resources, hence the finally block. + await _stream.DisposeAsync().ConfigureAwait(false); + } + finally + { + _disposed = true; + _charLen = 0; + await base.DisposeAsync().ConfigureAwait(false); + } + } + } + public override void Flush() { CheckAsyncTaskInProgress(); 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 179a07e0a956e4..63e411a8e45bb9 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/TextReader.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/TextReader.cs @@ -17,7 +17,7 @@ namespace System.IO // // This class is intended for character input, not bytes. // There are methods on the Stream class for reading bytes. - public abstract partial class TextReader : MarshalByRefObject, IDisposable + public abstract partial class TextReader : MarshalByRefObject, IDisposable, IAsyncDisposable { // Create our own instance to avoid static field initialization order problems on Mono. public static readonly TextReader Null = new StreamReader.NullStreamReader(); @@ -40,6 +40,19 @@ protected virtual void Dispose(bool disposing) { } + public virtual ValueTask DisposeAsync() + { + try + { + Dispose(); + return default; + } + catch (Exception exc) + { + return ValueTask.FromException(exc); + } + } + // Returns the next available character without actually reading it from // the input stream. The current position of the TextReader is not changed by // this operation. The returned value is -1 if no further characters are diff --git a/src/libraries/System.Runtime/ref/System.Runtime.cs b/src/libraries/System.Runtime/ref/System.Runtime.cs index 958f48681a09db..71430f5c23041f 100644 --- a/src/libraries/System.Runtime/ref/System.Runtime.cs +++ b/src/libraries/System.Runtime/ref/System.Runtime.cs @@ -10210,6 +10210,7 @@ public StreamReader(string path, System.Text.Encoding encoding, bool detectEncod public override void Close() { } public void DiscardBufferedData() { } protected override void Dispose(bool disposing) { } + public override System.Threading.Tasks.ValueTask DisposeAsync() { throw null; } public override int Peek() { throw null; } public override int Read() { throw null; } public override int Read(char[] buffer, int index, int count) { throw null; } @@ -10325,13 +10326,14 @@ public override void WriteLine(System.Text.StringBuilder? value) { } public override System.Threading.Tasks.Task WriteLineAsync(string? value) { throw null; } public override System.Threading.Tasks.Task WriteLineAsync(System.Text.StringBuilder? value, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } } - public abstract partial class TextReader : System.MarshalByRefObject, System.IDisposable + public abstract partial class TextReader : System.MarshalByRefObject, System.IAsyncDisposable, System.IDisposable { public static readonly System.IO.TextReader Null; protected TextReader() { } public virtual void Close() { } public void Dispose() { } protected virtual void Dispose(bool disposing) { } + public virtual System.Threading.Tasks.ValueTask DisposeAsync() { throw null; } public virtual int Peek() { throw null; } public virtual int Read() { throw null; } public virtual int Read(char[] buffer, int index, int count) { throw null; }