248 lines
7.3 KiB
C#
248 lines
7.3 KiB
C#
using System;
|
|
using System.IO;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace FluentTerminal.SystemTray
|
|
{
|
|
internal sealed class BufferedReader : IDisposable
|
|
{
|
|
private const int MaxTotalDelayMilliseconds = 100;
|
|
private const int WaitPeriodMilliseconds = 30;
|
|
private const int NearReadsPeriodMilliseconds = 20;
|
|
private const int NearReadsBufferingTrigger = 3;
|
|
private const int BufferSize = 204800;
|
|
|
|
private readonly object _lock = new object();
|
|
|
|
private readonly Stream _stream;
|
|
private readonly Action<byte[]> _callback;
|
|
private readonly bool _enableBuffer;
|
|
|
|
private bool _disposed;
|
|
private bool _paused;
|
|
|
|
private byte[] _buffer;
|
|
private int _bufferIndex;
|
|
private DateTime _lastRead;
|
|
private DateTime _sendingDeadline;
|
|
private DateTime _scheduledSend;
|
|
private int _nearReadingsCount;
|
|
private Task _sendingTask;
|
|
|
|
internal BufferedReader(Stream stream, Action<byte[]> callback, bool enableBuffer)
|
|
{
|
|
_stream = stream;
|
|
_callback = callback;
|
|
_enableBuffer = enableBuffer;
|
|
|
|
// ReSharper disable once AssignmentIsFullyDiscarded
|
|
_ = Task.Factory.StartNew(ReadingLoop, TaskCreationOptions.LongRunning);
|
|
}
|
|
|
|
internal void SetPaused(bool value)
|
|
{
|
|
lock (_lock) _paused = value;
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
lock (_lock) _disposed = true;
|
|
}
|
|
|
|
private async Task ReadingLoop()
|
|
{
|
|
while (true)
|
|
{
|
|
// Allow CPU to jump between TerminalSessions' ReadingLoop Tasks
|
|
await Task.Delay(1).ConfigureAwait(false);
|
|
|
|
bool paused;
|
|
|
|
lock (_lock)
|
|
{
|
|
if (_disposed) return;
|
|
|
|
paused = _paused;
|
|
}
|
|
|
|
if (paused)
|
|
{
|
|
await Task.Delay(WaitPeriodMilliseconds).ConfigureAwait(false);
|
|
|
|
continue;
|
|
}
|
|
|
|
var currentBuffer = new byte[BufferSize];
|
|
|
|
int read;
|
|
|
|
try
|
|
{
|
|
read = await _stream.ReadAsync(currentBuffer, 0, currentBuffer.Length).ConfigureAwait(false);
|
|
}
|
|
catch
|
|
{
|
|
read = 0;
|
|
}
|
|
|
|
if (read < 1)
|
|
{
|
|
// Expected to happen only when terminal is closed.
|
|
// Probably not recoverable, but we'll wait anyway 'till disposed.
|
|
await Task.Delay(50).ConfigureAwait(false);
|
|
|
|
continue;
|
|
}
|
|
|
|
if (!_enableBuffer)
|
|
{
|
|
// Buffering disabled. Just send.
|
|
|
|
_buffer = currentBuffer;
|
|
_bufferIndex = read;
|
|
|
|
SendBuffer();
|
|
|
|
continue;
|
|
}
|
|
|
|
var now = DateTime.UtcNow;
|
|
|
|
lock (_lock)
|
|
{
|
|
if (_buffer != null)
|
|
{
|
|
// We're already in buffered mode
|
|
|
|
if (_bufferIndex + read > BufferSize)
|
|
{
|
|
// No room in the buffer. Have to flush it.
|
|
SendBuffer();
|
|
|
|
_buffer = currentBuffer;
|
|
_bufferIndex = 0;
|
|
|
|
_scheduledSend = now.AddMilliseconds(WaitPeriodMilliseconds);
|
|
_sendingDeadline = now.AddMilliseconds(MaxTotalDelayMilliseconds);
|
|
}
|
|
else
|
|
{
|
|
// Copy to existing buffer
|
|
Buffer.BlockCopy(currentBuffer, 0, _buffer, _bufferIndex, read);
|
|
_bufferIndex += read;
|
|
|
|
_scheduledSend = now.AddMilliseconds(WaitPeriodMilliseconds);
|
|
}
|
|
|
|
if (now.Subtract(_lastRead).TotalMilliseconds < NearReadsPeriodMilliseconds)
|
|
{
|
|
// We should stop buffered mode
|
|
SendBuffer();
|
|
}
|
|
|
|
_lastRead = now;
|
|
|
|
continue;
|
|
}
|
|
|
|
if (now.Subtract(_lastRead).TotalMilliseconds < NearReadsPeriodMilliseconds)
|
|
{
|
|
_nearReadingsCount++;
|
|
}
|
|
else
|
|
{
|
|
_nearReadingsCount = 0;
|
|
}
|
|
|
|
_lastRead = now;
|
|
|
|
if (_nearReadingsCount >= NearReadsBufferingTrigger)
|
|
{
|
|
// We should enter buffered mode
|
|
_buffer = currentBuffer;
|
|
_bufferIndex = 0;
|
|
_sendingDeadline = now.AddMilliseconds(MaxTotalDelayMilliseconds);
|
|
_scheduledSend = now.AddMilliseconds(WaitPeriodMilliseconds);
|
|
|
|
if (_sendingTask == null) _sendingTask = SendAsync();
|
|
|
|
_nearReadingsCount = 0;
|
|
|
|
continue;
|
|
}
|
|
|
|
// Not in buffering mode. Just send.
|
|
_buffer = currentBuffer;
|
|
_bufferIndex = read;
|
|
|
|
SendBuffer();
|
|
}
|
|
}
|
|
}
|
|
|
|
private async Task SendAsync()
|
|
{
|
|
// Just to release the calling thread asap
|
|
await Task.Delay(5).ConfigureAwait(false);
|
|
|
|
while (true)
|
|
{
|
|
TimeSpan sleep;
|
|
|
|
lock (_lock)
|
|
{
|
|
if (_buffer == null)
|
|
{
|
|
_sendingTask = null;
|
|
|
|
return;
|
|
}
|
|
|
|
if (_paused)
|
|
{
|
|
sleep = TimeSpan.FromMilliseconds(WaitPeriodMilliseconds);
|
|
}
|
|
else
|
|
{
|
|
sleep = _scheduledSend < _sendingDeadline
|
|
? _scheduledSend.Subtract(DateTime.UtcNow)
|
|
: _sendingDeadline.Subtract(DateTime.UtcNow);
|
|
|
|
if (sleep.TotalMilliseconds < 5)
|
|
{
|
|
// Time to send
|
|
SendBuffer();
|
|
|
|
_sendingTask = null;
|
|
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
await Task.Delay(sleep).ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
// Has to be called from a locked code!
|
|
private void SendBuffer()
|
|
{
|
|
if (_bufferIndex == _buffer.Length)
|
|
{
|
|
_callback(_buffer);
|
|
}
|
|
else
|
|
{
|
|
var newBuffer = new byte[_bufferIndex];
|
|
|
|
Buffer.BlockCopy(_buffer, 0, newBuffer, 0, _bufferIndex);
|
|
|
|
_callback(newBuffer);
|
|
}
|
|
|
|
_buffer = null;
|
|
_bufferIndex = 0;
|
|
}
|
|
}
|
|
}
|