diff --git a/CliFx/Infrastructure/ConsoleReader.cs b/CliFx/Infrastructure/ConsoleReader.cs
index 35cb92c..6d5a8de 100644
--- a/CliFx/Infrastructure/ConsoleReader.cs
+++ b/CliFx/Infrastructure/ConsoleReader.cs
@@ -1,11 +1,16 @@
-using System.IO;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Runtime.CompilerServices;
using System.Text;
+using System.Threading.Tasks;
namespace CliFx.Infrastructure;
///
/// Implements a for reading characters from a console stream.
///
+// Both the underlying stream AND the stream reader must be synchronized!
+// https://github.com/Tyrrrz/CliFx/issues/123
public partial class ConsoleReader : StreamReader
{
///
@@ -29,6 +34,77 @@ public partial class ConsoleReader : StreamReader
: this(console, stream, System.Console.InputEncoding)
{
}
+
+ // The following overrides are required to establish thread-safe behavior
+ // in methods deriving from StreamReader.
+
+ ///
+ [ExcludeFromCodeCoverage, MethodImpl(MethodImplOptions.Synchronized)]
+ public override int Peek() => base.Peek();
+
+ ///
+ [ExcludeFromCodeCoverage, MethodImpl(MethodImplOptions.Synchronized)]
+ public override int Read() => base.Read();
+
+ ///
+ [ExcludeFromCodeCoverage, MethodImpl(MethodImplOptions.Synchronized)]
+ public override int Read(char[] buffer, int index, int count) =>
+ base.Read(buffer, index, count);
+
+ ///
+ [ExcludeFromCodeCoverage, MethodImpl(MethodImplOptions.Synchronized)]
+ public override int ReadBlock(char[] buffer, int index, int count)
+ {
+ return base.ReadBlock(buffer, index, count);
+ }
+
+ ///
+ [ExcludeFromCodeCoverage, MethodImpl(MethodImplOptions.Synchronized)]
+ public override string? ReadLine() => base.ReadLine();
+
+ ///
+ [ExcludeFromCodeCoverage, MethodImpl(MethodImplOptions.Synchronized)]
+ public override string ReadToEnd() => base.ReadToEnd();
+
+ ///
+ [ExcludeFromCodeCoverage, MethodImpl(MethodImplOptions.Synchronized)]
+ public override Task ReadAsync(char[] buffer, int index, int count)
+ {
+ // Must be non-async to work with locks
+ return Task.FromResult(Read(buffer, index, count));
+ }
+
+ ///
+ [ExcludeFromCodeCoverage, MethodImpl(MethodImplOptions.Synchronized)]
+ public override Task ReadBlockAsync(char[] buffer, int index, int count)
+ {
+ // Must be non-async to work with locks
+ return Task.FromResult(ReadBlock(buffer, index, count));
+ }
+
+ ///
+ [ExcludeFromCodeCoverage, MethodImpl(MethodImplOptions.Synchronized)]
+ public override Task ReadLineAsync()
+ {
+ // Must be non-async to work with locks
+ return Task.FromResult(ReadLine());
+ }
+
+ ///
+ [ExcludeFromCodeCoverage, MethodImpl(MethodImplOptions.Synchronized)]
+ public override Task ReadToEndAsync()
+ {
+ // Must be non-async to work with locks
+ return Task.FromResult(ReadToEnd());
+ }
+
+ ///
+ [ExcludeFromCodeCoverage, MethodImpl(MethodImplOptions.Synchronized)]
+ public override void Close() => base.Close();
+
+ ///
+ [ExcludeFromCodeCoverage, MethodImpl(MethodImplOptions.Synchronized)]
+ protected override void Dispose(bool disposing) => base.Dispose(disposing);
}
public partial class ConsoleReader
diff --git a/CliFx/Infrastructure/ConsoleWriter.cs b/CliFx/Infrastructure/ConsoleWriter.cs
index 08fc183..1873ea7 100644
--- a/CliFx/Infrastructure/ConsoleWriter.cs
+++ b/CliFx/Infrastructure/ConsoleWriter.cs
@@ -1,5 +1,8 @@
-using System.IO;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Runtime.CompilerServices;
using System.Text;
+using System.Threading.Tasks;
using CliFx.Utils;
namespace CliFx.Infrastructure;
@@ -7,6 +10,8 @@ namespace CliFx.Infrastructure;
///
/// Implements a for writing characters to a console stream.
///
+// Both the underlying stream AND the stream writer must be synchronized!
+// https://github.com/Tyrrrz/CliFx/issues/123
public partial class ConsoleWriter : StreamWriter
{
///
@@ -30,6 +35,239 @@ public partial class ConsoleWriter : StreamWriter
: this(console, stream, System.Console.OutputEncoding)
{
}
+
+ // The following overrides are required to establish thread-safe behavior
+ // in methods deriving from StreamWriter.
+
+ ///
+ [ExcludeFromCodeCoverage, MethodImpl(MethodImplOptions.Synchronized)]
+ public override void Write(char[] buffer, int index, int count) => base.Write(buffer, index, count);
+
+ ///
+ [ExcludeFromCodeCoverage, MethodImpl(MethodImplOptions.Synchronized)]
+ public override void Write(char[] buffer) => base.Write(buffer);
+
+ ///
+ [ExcludeFromCodeCoverage, MethodImpl(MethodImplOptions.Synchronized)]
+ public override void Write(char value) => base.Write(value);
+
+ ///
+ [ExcludeFromCodeCoverage, MethodImpl(MethodImplOptions.Synchronized)]
+ public override void Write(string? value) => base.Write(value);
+
+ ///
+ [ExcludeFromCodeCoverage, MethodImpl(MethodImplOptions.Synchronized)]
+ public override void Write(string format, object? arg0) => base.Write(format, arg0);
+
+ ///
+ [ExcludeFromCodeCoverage, MethodImpl(MethodImplOptions.Synchronized)]
+ public override void Write(string format, object? arg0, object? arg1) =>
+ base.Write(format, arg0, arg1);
+
+ ///
+ [ExcludeFromCodeCoverage, MethodImpl(MethodImplOptions.Synchronized)]
+ public override void Write(string format, object? arg0, object? arg1, object? arg2) =>
+ base.Write(format, arg0, arg1, arg2);
+
+ ///
+ [ExcludeFromCodeCoverage, MethodImpl(MethodImplOptions.Synchronized)]
+ public override void Write(string format, params object?[] arg) => base.Write(format, arg);
+
+ ///
+ [ExcludeFromCodeCoverage, MethodImpl(MethodImplOptions.Synchronized)]
+ public override void Write(bool value) => base.Write(value);
+
+ ///
+ [ExcludeFromCodeCoverage, MethodImpl(MethodImplOptions.Synchronized)]
+ public override void Write(int value) => base.Write(value);
+
+ ///
+ [ExcludeFromCodeCoverage, MethodImpl(MethodImplOptions.Synchronized)]
+ public override void Write(long value) => base.Write(value);
+
+ ///
+ [ExcludeFromCodeCoverage, MethodImpl(MethodImplOptions.Synchronized)]
+ public override void Write(uint value) => base.Write(value);
+
+ ///
+ [ExcludeFromCodeCoverage, MethodImpl(MethodImplOptions.Synchronized)]
+ public override void Write(ulong value) => base.Write(value);
+
+ ///
+ [ExcludeFromCodeCoverage, MethodImpl(MethodImplOptions.Synchronized)]
+ public override void Write(float value) => base.Write(value);
+
+ ///
+ [ExcludeFromCodeCoverage, MethodImpl(MethodImplOptions.Synchronized)]
+ public override void Write(double value) => base.Write(value);
+
+ ///
+ [ExcludeFromCodeCoverage, MethodImpl(MethodImplOptions.Synchronized)]
+ public override void Write(decimal value) => base.Write(value);
+
+ ///
+ [ExcludeFromCodeCoverage, MethodImpl(MethodImplOptions.Synchronized)]
+ public override void Write(object? value) => base.Write(value);
+
+ ///
+ [ExcludeFromCodeCoverage, MethodImpl(MethodImplOptions.Synchronized)]
+ public override Task WriteAsync(char[] buffer, int index, int count)
+ {
+ // Must be non-async to work with locks
+ Write(buffer, index, count);
+ return Task.CompletedTask;
+ }
+
+ ///
+ [ExcludeFromCodeCoverage, MethodImpl(MethodImplOptions.Synchronized)]
+ public override Task WriteAsync(char value)
+ {
+ // Must be non-async to work with locks
+ Write(value);
+ return Task.CompletedTask;
+ }
+
+ ///
+ [ExcludeFromCodeCoverage, MethodImpl(MethodImplOptions.Synchronized)]
+ public override Task WriteAsync(string? value)
+ {
+ // Must be non-async to work with locks
+ Write(value);
+ return Task.CompletedTask;
+ }
+
+ ///
+ [ExcludeFromCodeCoverage, MethodImpl(MethodImplOptions.Synchronized)]
+ public override void WriteLine() => base.WriteLine();
+
+ ///
+ [ExcludeFromCodeCoverage, MethodImpl(MethodImplOptions.Synchronized)]
+ public override void WriteLine(char[] buffer, int index, int count) =>
+ base.WriteLine(buffer, index, count);
+
+ ///
+ [ExcludeFromCodeCoverage, MethodImpl(MethodImplOptions.Synchronized)]
+ public override void WriteLine(char[] buffer) => base.WriteLine(buffer);
+
+ ///
+ [ExcludeFromCodeCoverage, MethodImpl(MethodImplOptions.Synchronized)]
+ public override void WriteLine(char value) => base.WriteLine(value);
+
+ ///
+ [ExcludeFromCodeCoverage, MethodImpl(MethodImplOptions.Synchronized)]
+ public override void WriteLine(string? value) => base.WriteLine(value);
+
+ ///
+ [ExcludeFromCodeCoverage, MethodImpl(MethodImplOptions.Synchronized)]
+ public override void WriteLine(string format, object? arg0) => base.WriteLine(format, arg0);
+
+ ///
+ [ExcludeFromCodeCoverage, MethodImpl(MethodImplOptions.Synchronized)]
+ public override void WriteLine(string format, object? arg0, object? arg1) =>
+ base.WriteLine(format, arg0, arg1);
+
+ ///
+ [ExcludeFromCodeCoverage, MethodImpl(MethodImplOptions.Synchronized)]
+ public override void WriteLine(string format, object? arg0, object? arg1, object? arg2) =>
+ base.WriteLine(format, arg0, arg1, arg2);
+
+ ///
+ [ExcludeFromCodeCoverage, MethodImpl(MethodImplOptions.Synchronized)]
+ public override void WriteLine(string format, params object?[] arg) =>
+ base.WriteLine(format, arg);
+
+ ///
+ [ExcludeFromCodeCoverage, MethodImpl(MethodImplOptions.Synchronized)]
+ public override void WriteLine(bool value) => base.WriteLine(value);
+
+ ///
+ [ExcludeFromCodeCoverage, MethodImpl(MethodImplOptions.Synchronized)]
+ public override void WriteLine(int value) => base.WriteLine(value);
+
+ ///
+ [ExcludeFromCodeCoverage, MethodImpl(MethodImplOptions.Synchronized)]
+ public override void WriteLine(long value) => base.WriteLine(value);
+
+ ///
+ [ExcludeFromCodeCoverage, MethodImpl(MethodImplOptions.Synchronized)]
+ public override void WriteLine(uint value) => base.WriteLine(value);
+
+ ///
+ [ExcludeFromCodeCoverage, MethodImpl(MethodImplOptions.Synchronized)]
+ public override void WriteLine(ulong value) => base.WriteLine(value);
+
+ ///
+ [ExcludeFromCodeCoverage, MethodImpl(MethodImplOptions.Synchronized)]
+ public override void WriteLine(float value) => base.WriteLine(value);
+
+ ///
+ [ExcludeFromCodeCoverage, MethodImpl(MethodImplOptions.Synchronized)]
+ public override void WriteLine(double value) => base.WriteLine(value);
+
+ ///
+ [ExcludeFromCodeCoverage, MethodImpl(MethodImplOptions.Synchronized)]
+ public override void WriteLine(decimal value) => base.WriteLine(value);
+
+ ///
+ [ExcludeFromCodeCoverage, MethodImpl(MethodImplOptions.Synchronized)]
+ public override void WriteLine(object? value) => base.WriteLine(value);
+
+ ///
+ [ExcludeFromCodeCoverage, MethodImpl(MethodImplOptions.Synchronized)]
+ public override Task WriteLineAsync()
+ {
+ // Must be non-async to work with locks
+ WriteLine();
+ return Task.CompletedTask;
+ }
+
+ ///
+ [ExcludeFromCodeCoverage, MethodImpl(MethodImplOptions.Synchronized)]
+ public override Task WriteLineAsync(char value)
+ {
+ // Must be non-async to work with locks
+ WriteLine(value);
+ return Task.CompletedTask;
+ }
+
+ ///
+ [ExcludeFromCodeCoverage, MethodImpl(MethodImplOptions.Synchronized)]
+ public override Task WriteLineAsync(char[] buffer, int index, int count)
+ {
+ // Must be non-async to work with locks
+ WriteLine(buffer, index, count);
+ return Task.CompletedTask;
+ }
+
+ ///
+ [ExcludeFromCodeCoverage, MethodImpl(MethodImplOptions.Synchronized)]
+ public override Task WriteLineAsync(string? value)
+ {
+ // Must be non-async to work with locks
+ WriteLine(value);
+ return Task.CompletedTask;
+ }
+
+ ///
+ [ExcludeFromCodeCoverage, MethodImpl(MethodImplOptions.Synchronized)]
+ public override void Flush() => base.Flush();
+
+ ///
+ [ExcludeFromCodeCoverage, MethodImpl(MethodImplOptions.Synchronized)]
+ public override Task FlushAsync()
+ {
+ // Must be non-async to work with locks
+ Flush();
+ return Task.CompletedTask;
+ }
+
+ ///
+ [ExcludeFromCodeCoverage, MethodImpl(MethodImplOptions.Synchronized)]
+ public override void Close() => base.Close();
+
+ ///
+ [ExcludeFromCodeCoverage, MethodImpl(MethodImplOptions.Synchronized)]
+ protected override void Dispose(bool disposing) => base.Dispose(disposing);
}
public partial class ConsoleWriter
diff --git a/CliFx/Utils/NoPreambleEncoding.cs b/CliFx/Utils/NoPreambleEncoding.cs
index 9f35c19..1053cb7 100644
--- a/CliFx/Utils/NoPreambleEncoding.cs
+++ b/CliFx/Utils/NoPreambleEncoding.cs
@@ -8,6 +8,8 @@ namespace CliFx.Utils;
// https://github.com/dotnet/runtime/blob/01b7e73cd378145264a7cb7a09365b41ed42b240/src/libraries/Common/src/System/Text/ConsoleEncoding.cs
// Also see:
// https://source.dot.net/#System.Console/ConsoleEncoding.cs,5eedd083a4a4f4a2
+// Majority of overrides are just proxy calls to avoid potentially more expensive base behavior.
+// The important part is the GetPreamble() method that has been overriden to return an empty array.
internal class NoPreambleEncoding : Encoding
{
private readonly Encoding _underlyingEncoding;
@@ -72,20 +74,20 @@ internal class NoPreambleEncoding : Encoding
public override int GetBytes(char[] chars, int charIndex, int charCount, byte[] bytes, int byteIndex) =>
_underlyingEncoding.GetBytes(chars, charIndex, charCount, bytes, byteIndex);
- [ExcludeFromCodeCoverage]
- public override byte[] GetBytes(char[] chars) => _underlyingEncoding.GetBytes(chars);
-
[ExcludeFromCodeCoverage]
public override byte[] GetBytes(char[] chars, int index, int count) =>
_underlyingEncoding.GetBytes(chars, index, count);
[ExcludeFromCodeCoverage]
- public override byte[] GetBytes(string s) => _underlyingEncoding.GetBytes(s);
+ public override byte[] GetBytes(char[] chars) => _underlyingEncoding.GetBytes(chars);
[ExcludeFromCodeCoverage]
public override int GetBytes(string s, int charIndex, int charCount, byte[] bytes, int byteIndex) =>
_underlyingEncoding.GetBytes(s, charIndex, charCount, bytes, byteIndex);
+ [ExcludeFromCodeCoverage]
+ public override byte[] GetBytes(string s) => _underlyingEncoding.GetBytes(s);
+
[ExcludeFromCodeCoverage]
public override int GetCharCount(byte[] bytes, int index, int count) =>
_underlyingEncoding.GetCharCount(bytes, index, count);