Is there an in memory stream that blocks like a file stream
Asked Answered
N

6

18

I'm using a library that requires I provide an object that implements this interface:

public interface IConsole {
    TextWriter StandardInput { get; }
    TextReader StandardOutput { get; }
    TextReader StandardError { get; }
}

The object's readers then get used by the library with:

IConsole console = new MyConsole();
int readBytes = console.StandardOutput.Read(buffer, 0, buffer.Length);

Normally the class implementing IConsole has the StandardOutput stream as coming from an external process. In that case the console.StandardOutput.Read calls work by blocking until there is some data written to the StandardOutput stream.

What I'm trying to do is create a test IConsole implementation that uses MemoryStreams and echo's whatever appears on the StandardInput back onto the StandardInput. I tried:

MemoryStream echoOutStream = new MemoryStream();
StandardOutput = new StreamReader(echoOutStream);

But the problem with that is the console.StandardOutput.Read will return 0 rather than block until there is some data. Is there anyway I can get a MemoryStream to block if there is no data available or is there a different in memory stream I could use?

Nice answered 25/9, 2009 at 6:33 Comment(4)
You really shouldn't be reading from an output stream.Blooper
I can't believe .net doesn't have this built in. I'm taking a SQL VDI that writes raw backup data to a .NET stream, and then I need to read from that stream and write it to a remote powershell PSSession output. Unfortunately, I can't seem to read the stream because it always returns 0.Roxana
@Martinv.Löwis It's never about what you shouldn't be doing. It's about what you can do with the tools given. Being able to read and write from the same stream should be easy to do, but it's not.Roxana
You need to be able to read from the underlying stream of Console.Out and Console.Error if you're redirecting them to say, a MemoryStream.Tugboat
N
10

In the end I found an easy way to do it by inheriting from MemoryStream and taking over the Read and Write methods.

public class EchoStream : MemoryStream {

    private ManualResetEvent m_dataReady = new ManualResetEvent(false);
    private byte[] m_buffer;
    private int m_offset;
    private int m_count;

    public override void Write(byte[] buffer, int offset, int count) {
        m_buffer = buffer;
        m_offset = offset;
        m_count = count;
        m_dataReady.Set();
    }

    public override int Read(byte[] buffer, int offset, int count) {
        if (m_buffer == null) {
            // Block until the stream has some more data.
            m_dataReady.Reset();
            m_dataReady.WaitOne();    
        }

        Buffer.BlockCopy(m_buffer, m_offset, buffer, offset, (count < m_count) ? count : m_count);
        m_buffer = null;
        return (count < m_count) ? count : m_count;
    }
}
Nice answered 28/9, 2009 at 0:52 Comment(4)
You have a race condition in Read(). If Write() is called by another thread between the null check of the buffer and m_dataReady.Reset(), you may potentially have to wait forever if the server will not send data again. In most request/response protocols, this would create a dead lock. I suggest you use a automatic event instead.Bolter
Fair enough. I'd agree it's worth putting a check in for. Needless to say I've had the above code in production on a public facing SSH service for 5 years and have never had the service hang so I suspect it's a very low probability condition.Nice
@sipwiz this is a good answer. However, the use of Array.copy does not allow the read to function properly. It won't support offset copies. You need change to Buffer.BlockCopy(m_buffer, 0, buffer, offset, m_count); this is also faster on most systems.Allan
This version isn't going to work out very well if the Read buffer is smaller than the write. While it won't throw an exception like the function above, this one will just start lopping off data.Roxana
O
14

Inspired by your answer, here's my multi-thread, multi-write version:

public class EchoStream : MemoryStream
{
    private readonly ManualResetEvent _DataReady = new ManualResetEvent(false);
    private readonly ConcurrentQueue<byte[]> _Buffers = new ConcurrentQueue<byte[]>();

    public bool DataAvailable{get { return !_Buffers.IsEmpty; }}

    public override void Write(byte[] buffer, int offset, int count)
    {
        _Buffers.Enqueue(buffer);
        _DataReady.Set();
    }

    public override int Read(byte[] buffer, int offset, int count)
    {
        _DataReady.WaitOne();

        byte[] lBuffer;

        if (!_Buffers.TryDequeue(out lBuffer))
        {
            _DataReady.Reset();
            return -1;
        }

        if (!DataAvailable)
            _DataReady.Reset();

        Array.Copy(lBuffer, buffer, lBuffer.Length);
        return lBuffer.Length;
    }
}

With your version you should Read the Stream upon Write, without any consecutively write be possible. My version buffers any written buffer in a ConcurrentQueue (it's fairly simple to change it to a simple Queue and lock it)

Oeillade answered 2/10, 2013 at 12:54 Comment(3)
this is awesome, however there's a bug in Write method, _Buffers.Enqueue(buffer); should be replaced with _Buffers.Enqueue(buffer.Take(count).ToArray()); and then it's really working, blocking and exchanging data between threads! Thanks!Morocco
Further, should be _Buffers.Enqueue(buffer.Skip(offset).Take(count).ToArray()); And note that Read currently ignores offset and count (to implement, replace ConcurrentQueue<byte[]> with ConcurrentQueue<byte> and TryDequeue in a loop until you have enough or there are none left)Menswear
This may not work out very well if you attempt to read with a buffer size smaller than the write. The Array.Copy will throw an exception if there's not enough room...Roxana
N
10

In the end I found an easy way to do it by inheriting from MemoryStream and taking over the Read and Write methods.

public class EchoStream : MemoryStream {

    private ManualResetEvent m_dataReady = new ManualResetEvent(false);
    private byte[] m_buffer;
    private int m_offset;
    private int m_count;

    public override void Write(byte[] buffer, int offset, int count) {
        m_buffer = buffer;
        m_offset = offset;
        m_count = count;
        m_dataReady.Set();
    }

    public override int Read(byte[] buffer, int offset, int count) {
        if (m_buffer == null) {
            // Block until the stream has some more data.
            m_dataReady.Reset();
            m_dataReady.WaitOne();    
        }

        Buffer.BlockCopy(m_buffer, m_offset, buffer, offset, (count < m_count) ? count : m_count);
        m_buffer = null;
        return (count < m_count) ? count : m_count;
    }
}
Nice answered 28/9, 2009 at 0:52 Comment(4)
You have a race condition in Read(). If Write() is called by another thread between the null check of the buffer and m_dataReady.Reset(), you may potentially have to wait forever if the server will not send data again. In most request/response protocols, this would create a dead lock. I suggest you use a automatic event instead.Bolter
Fair enough. I'd agree it's worth putting a check in for. Needless to say I've had the above code in production on a public facing SSH service for 5 years and have never had the service hang so I suspect it's a very low probability condition.Nice
@sipwiz this is a good answer. However, the use of Array.copy does not allow the read to function properly. It won't support offset copies. You need change to Buffer.BlockCopy(m_buffer, 0, buffer, offset, m_count); this is also faster on most systems.Allan
This version isn't going to work out very well if the Read buffer is smaller than the write. While it won't throw an exception like the function above, this one will just start lopping off data.Roxana
R
4

I'm going to add one more refined version of EchoStream. This is a combination of the other two versions, plus some suggestions from the comments.

UPDATE - I have tested this EchoStream with over 50 terrabytes of data run through it for days on end. The test had it sitting between a network stream and the ZStandard compression stream. The async has also been tested, which brought a rare hanging condition to the surface. It appears the built in System.IO.Stream does not expect one to call both ReadAsync and WriteAsync on the same stream at the same time, which can cause it to hang if there isn't any data available because both calls utilize the same internal variables. Therefore I had to override those functions, which resolved the hanging issue.

This version has the following enhancements:

  1. This was written from scratch using the System.IO.Stream base class instead of MemoryStream.

  2. The constructor can set a max queue depth and if this level is reached then stream writes will block until a Read is performed which drops the queue depth back below the max level (no limit=0, default=10).

  3. When reading/writing data, the buffer offset and count are now honored. Also, you can call Read with a smaller buffer than Write without throwing an exception or losing data. BlockCopy is used in a loop to fill in the bytes until count is satisfied.

  4. There is a public property called AlwaysCopyBuffer, which makes a copy of the buffer in the Write function. Setting this to true will safely allow the byte buffer to be reused after calling Write.

  5. There is a public property called ReadTimeout/WriteTimeout, which controls how long the Read/Write function will block before it returns 0 (default=Infinite, -1).

  6. The BlockingCollection<> class is used, which under the hood combines the ConcurrentQueue and AutoResetEvent classes. Originally I was using these two classes, but there exists a rare condition where you will find that after data has been Enqueued( ), that it is not available immediately when AutoResetEvent allows a thread through in the Read( ). This happens about once every 500GB of data that passes through it. The cure was to Sleep and check for the data again. Sometimes a Sleep(0) worked, but in extreme cases where the CPU usage was high, it could be as high as Sleep(1000) before the data showed up. After I switched to BlockingCollection<>, it has a lot of extra code to handle off of this elegantly and without issues.

  7. This has been tested to be thread safe for simultaneous async reads and writes.

using System;
using System.IO;
using System.Threading.Tasks;
using System.Threading;
using System.Collections.Concurrent;

public class EchoStream : Stream
{
    public override bool CanTimeout { get; } = true;
    public override int ReadTimeout { get; set; } = Timeout.Infinite;
    public override int WriteTimeout { get; set; } = Timeout.Infinite;
    public override bool CanRead { get; } = true;
    public override bool CanSeek { get; } = false;
    public override bool CanWrite { get; } = true;

    public bool CopyBufferOnWrite { get; set; } = false;

    private readonly object _lock = new object();

    // Default underlying mechanism for BlockingCollection is ConcurrentQueue<T>, which is what we want
    private readonly BlockingCollection<byte[]> _Buffers;
    private int _maxQueueDepth = 10;

    private byte[] m_buffer = null;
    private int m_offset = 0;
    private int m_count = 0;

    private bool m_Closed = false;
    private bool m_FinalZero = false; //after the stream is closed, set to true after returning a 0 for read()
    public override void Close()
    {
        m_Closed = true;

        // release any waiting writes
        _Buffers.CompleteAdding();
    }

    public bool DataAvailable
    {
        get
        {
            return _Buffers.Count > 0;
        }
    }

    private long _Length = 0L;
    public override long Length
    {
        get
        {
            return _Length;
        }
    }

    private long _Position = 0L;
    public override long Position
    {
        get
        {
            return _Position;
        }
        set
        {
            throw new NotImplementedException();
        }
    }

    public EchoStream() : this(10)
    {
    }

    public EchoStream(int maxQueueDepth)
    {
        _maxQueueDepth = maxQueueDepth;
        _Buffers = new BlockingCollection<byte[]>(_maxQueueDepth);
    }

    // we override the xxxxAsync functions because the default base class shares state between ReadAsync and WriteAsync, which causes a hang if both are called at once
    public new Task WriteAsync(byte[] buffer, int offset, int count)
    {
        return Task.Run(() => Write(buffer, offset, count));
    }

    // we override the xxxxAsync functions because the default base class shares state between ReadAsync and WriteAsync, which causes a hang if both are called at once
    public new Task<int> ReadAsync(byte[] buffer, int offset, int count)
    {
        return Task.Run(() =>
        {
            return Read(buffer, offset, count);
        });
    }

    public override void Write(byte[] buffer, int offset, int count)
    {
        if (m_Closed || buffer.Length - offset < count || count <= 0)
            return;

        byte[] newBuffer;
        if (!CopyBufferOnWrite && offset == 0 && count == buffer.Length)
            newBuffer = buffer;
        else
        {
            newBuffer = new byte[count];
            System.Buffer.BlockCopy(buffer, offset, newBuffer, 0, count);
        }
        if (!_Buffers.TryAdd(newBuffer, WriteTimeout))
            throw new TimeoutException("EchoStream Write() Timeout");

        _Length += count;
    }

    public override int Read(byte[] buffer, int offset, int count)
    {
        if (count == 0)
            return 0;
        lock (_lock)
        {
            if (m_count == 0 && _Buffers.Count == 0)
            {
                if (m_Closed)
                {
                    if (!m_FinalZero)
                    {
                        m_FinalZero = true;
                        return 0;
                    }
                    else
                    {
                        return -1;
                    }
                }

                if (_Buffers.TryTake(out m_buffer, ReadTimeout))
                {
                    m_offset = 0;
                    m_count = m_buffer.Length;
                }
                else
                {
                    if (m_Closed)
                    {
                        if (!m_FinalZero)
                        {
                            m_finalZero = true;
                            return 0;
                        }
                        else
                        {
                            return -1;
                        }
                    }
                    else
                    {
                        return 0;
                    }
                }
            }

            int returnBytes = 0;
            while (count > 0)
            {
                if (m_count == 0)
                {
                    if (_Buffers.TryTake(out m_buffer, 0))
                    {
                        m_offset = 0;
                        m_count = m_buffer.Length;
                    }
                    else
                        break;
                }

                var bytesToCopy = (count < m_count) ? count : m_count;
                System.Buffer.BlockCopy(m_buffer, m_offset, buffer, offset, bytesToCopy);
                m_offset += bytesToCopy;
                m_count -= bytesToCopy;
                offset += bytesToCopy;
                count -= bytesToCopy;

                returnBytes += bytesToCopy;
            }

            _Position += returnBytes;

            return returnBytes;
        }
    }

    public override int ReadByte()
    {
        byte[] returnValue = new byte[1];
        return (Read(returnValue, 0, 1) <= 0 ? -1 : (int)returnValue[0]);
    }

    public override void Flush()
    {
    }

    public override long Seek(long offset, SeekOrigin origin)
    {
        throw new NotImplementedException();
    }

    public override void SetLength(long value)
    {
        throw new NotImplementedException();
    }
}
Roxana answered 5/12, 2018 at 6:57 Comment(9)
I added the function ReadByte: public override int ReadByte() { byte[] returnValue = new byte[1]; return (Read(returnValue,0,1) == 0 ? -1 : returnValue[0]); }Shiah
@MaxR. Changed "== 0" to "<= 0", to handle cases where Read returns -1Roxana
@Brain2000: Man, So much joy!! I would pay you a beerTalkfest
Small bug though, you never seem to decrement the Length by returnBytes.Talkfest
@PauloNeves Should I be decrementing the length? I was keeping a running total of writes in length, and a running total of reads in position. So I believe length - position would be the number of bytes available.Roxana
@Brain2000: +1 this is great work! I'm using it in a Xamarin app to provide a C# REPL using Mono.CSharp.Evaluator on iPhone and Roslyn CSharpScript on WPF and it works as a stand-in replacement for MemoryStream when I need to wait in the background in a Task for Console.ReadLine() input from a script.Tugboat
I was wondering about returnBytes also, in the meantime I just took out the NotImplementedException for the Position setter and accept any value. Not that it does anything useful, but I was using MemoryStream before and decrementing the Position after a write to Console.In from the app so that Console.ReadLine() would later pick it up.Tugboat
@JohnErnest The returnBytes keep track of how many bytes were copied into the buffer and is returned from the Read( ) function. For example, if there is only 1 byte available in the stream and you call "Read(buffer, 0, 1024)" with a 1k byte[] buffer, the integer 1 will be returned and that one byte will be in buffer[0]Roxana
I just fixed a bug where Read( ) would return a -1 when done instead of a 0, if Close( ) was called by the source before the destination finished retrieving all the data. The .net stream spec requires a stream to return 0 under all cases when complete, never a -1.Roxana
T
4

UPDATE: this works in .NET 4.8, but the behavior was changed in .NET Core and it no longer blocks the same way.

An anonymous pipe stream blocks like a file stream and should handle more edge cases than the sample code provided.

Here is a unit test that demonstrates this behavior.

var cts = new CancellationTokenSource();
using (var pipeServer = new AnonymousPipeServerStream(PipeDirection.Out))
using (var pipeStream = new AnonymousPipeClientStream(PipeDirection.In, pipeServer.ClientSafePipeHandle))
{
    var buffer = new byte[1024];
    var readTask = pipeStream.ReadAsync(buffer, 0, buffer.Length, cts.Token);
    Assert.IsFalse(readTask.IsCompleted, "Read already complete");

    // Cancelling does NOT unblock the read
    cts.Cancel();
    Assert.IsFalse(readTask.IsCanceled, "Read cancelled");

    // Only sending data does
    pipeServer.WriteByte(42);
    var bytesRead = await readTask;
    Assert.AreEqual(1, bytesRead);
}
Transformism answered 6/8, 2020 at 17:37 Comment(1)
This was the easiest way to setup my unit tests of some console app. Two pipes and stream writers with AutoFlush (and global lock to prevent parallelism). Done. Thanks for the idea.Audreyaudri
L
1

Here's my take on the EchoStream posted above. It handles the offset and count parameters on Write and Read.

public class EchoStream : MemoryStream
{
    private readonly ManualResetEvent _DataReady = new ManualResetEvent(false);
    private readonly ConcurrentQueue<byte[]> _Buffers = new ConcurrentQueue<byte[]>();

    public bool DataAvailable { get { return !_Buffers.IsEmpty; } }

    public override void Write(byte[] buffer, int offset, int count)
    {
        _Buffers.Enqueue(buffer.Skip(offset).Take(count).ToArray());
        _DataReady.Set();
    }

    public override int Read(byte[] buffer, int offset, int count)
    {
        _DataReady.WaitOne();

        byte[] lBuffer;

        if (!_Buffers.TryDequeue(out lBuffer))
        {
            _DataReady.Reset();
            return -1;
        }

        if (!DataAvailable)
            _DataReady.Reset();

        Array.Copy(lBuffer, 0, buffer, offset, Math.Min(lBuffer.Length, count));
        return lBuffer.Length;
    }
}

I was able to use this class to unit test a System.IO.Pipelines implementation. I needed a MemoryStream that could simulate multiple read calls in succession without reaching the end of the stream.

Lur answered 25/3, 2021 at 18:25 Comment(0)
S
1

I was trying to use all the codes from other answers, as well as the famous EchoStream, but unfortunately, they all were not working as I need:

  • EchoStream stream doesn't work well with non-standard read and write sizes, causing loss of data and corrupted reads.
  • EchoStream limits the stream by the number of writes, but not by the count, so theoretically someone can write in tons of data.

Solution: I've created a ThroughStream, which is limited by any specified exact buffer size. The actual size might grow up to bufferSize * 2, but not larger than that.

It works perfectly with any non-standard size reads and writes, doesn't fail in multithreading, and is quite simple and optimized.

And is available on Gist! (click here)

Successive answered 1/10, 2022 at 14:12 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.