Browse Source

Merge branch 'dev' of https://github.com/ionite34/StabilityMatrix into invoke-fixes

pull/240/head
JT 1 year ago
parent
commit
407e876bf4
  1. 46
      StabilityMatrix.Core/Processes/AnsiProcess.cs
  2. 105
      StabilityMatrix.Core/Processes/AsyncStreamReader.cs

46
StabilityMatrix.Core/Processes/AnsiProcess.cs

@ -10,22 +10,22 @@ public class AnsiProcess : Process
{ {
private AsyncStreamReader? stdoutReader; private AsyncStreamReader? stdoutReader;
private AsyncStreamReader? stderrReader; private AsyncStreamReader? stderrReader;
public AnsiProcess(ProcessStartInfo startInfo) public AnsiProcess(ProcessStartInfo startInfo)
{ {
StartInfo = startInfo; StartInfo = startInfo;
EnableRaisingEvents = false; EnableRaisingEvents = false;
StartInfo.UseShellExecute = false; StartInfo.UseShellExecute = false;
StartInfo.CreateNoWindow = true; StartInfo.CreateNoWindow = true;
StartInfo.RedirectStandardOutput = true; StartInfo.RedirectStandardOutput = true;
StartInfo.RedirectStandardInput = true; StartInfo.RedirectStandardInput = true;
StartInfo.RedirectStandardError = true; StartInfo.RedirectStandardError = true;
// Need this to parse ANSI escape sequences correctly // Need this to parse ANSI escape sequences correctly
StartInfo.StandardOutputEncoding = Encoding.UTF8; StartInfo.StandardOutputEncoding = new UTF8Encoding(false);
StartInfo.StandardErrorEncoding = Encoding.UTF8; StartInfo.StandardErrorEncoding = new UTF8Encoding(false);
StartInfo.StandardInputEncoding = Encoding.UTF8; StartInfo.StandardInputEncoding = new UTF8Encoding(false);
} }
/// <summary> /// <summary>
@ -35,23 +35,33 @@ public class AnsiProcess : Process
public void BeginAnsiRead(Action<ProcessOutput> callback) public void BeginAnsiRead(Action<ProcessOutput> callback)
{ {
var stdoutStream = StandardOutput.BaseStream; var stdoutStream = StandardOutput.BaseStream;
stdoutReader = new AsyncStreamReader(stdoutStream, s => stdoutReader = new AsyncStreamReader(
{ stdoutStream,
if (s == null) return; s =>
callback(ProcessOutput.FromStdOutLine(s)); {
}, StandardOutput.CurrentEncoding); if (s == null)
return;
callback(ProcessOutput.FromStdOutLine(s));
},
StandardOutput.CurrentEncoding
);
var stderrStream = StandardError.BaseStream; var stderrStream = StandardError.BaseStream;
stderrReader = new AsyncStreamReader(stderrStream, s => stderrReader = new AsyncStreamReader(
{ stderrStream,
if (s == null) return; s =>
callback(ProcessOutput.FromStdErrLine(s)); {
}, StandardError.CurrentEncoding); if (s == null)
return;
callback(ProcessOutput.FromStdErrLine(s));
},
StandardError.CurrentEncoding
);
stdoutReader.BeginReadLine(); stdoutReader.BeginReadLine();
stderrReader.BeginReadLine(); stderrReader.BeginReadLine();
} }
/// <summary> /// <summary>
/// Waits for output readers to finish /// Waits for output readers to finish
/// </summary> /// </summary>

105
StabilityMatrix.Core/Processes/AsyncStreamReader.cs

@ -13,17 +13,17 @@ namespace StabilityMatrix.Core.Processes;
/// <summary> /// <summary>
/// Modified from System.Diagnostics.AsyncStreamReader to support terminal processing. /// Modified from System.Diagnostics.AsyncStreamReader to support terminal processing.
/// ///
/// Currently has these modifications: /// Currently has these modifications:
/// - Carriage returns do not count as newlines '\r'. /// - Carriage returns do not count as newlines '\r'.
/// - APC messages are sent immediately without needing a newline. /// - APC messages are sent immediately without needing a newline.
/// ///
/// <seealso cref="ApcParser"/> /// <seealso cref="ApcParser"/>
/// </summary> /// </summary>
[SuppressMessage("ReSharper", "InconsistentNaming")] [SuppressMessage("ReSharper", "InconsistentNaming")]
internal sealed class AsyncStreamReader : IDisposable internal sealed class AsyncStreamReader : IDisposable
{ {
private const int DefaultBufferSize = 1024; // Byte buffer size private const int DefaultBufferSize = 1024; // Byte buffer size
private readonly Stream _stream; private readonly Stream _stream;
private readonly Decoder _decoder; private readonly Decoder _decoder;
@ -42,10 +42,10 @@ internal sealed class AsyncStreamReader : IDisposable
// Cache the last position scanned in sb when searching for lines. // Cache the last position scanned in sb when searching for lines.
private int _currentLinePos; private int _currentLinePos;
// (new) Flag to send next buffer immediately // (new) Flag to send next buffer immediately
private bool _sendNextBufferImmediately; private bool _sendNextBufferImmediately;
// Creates a new AsyncStreamReader for the given stream. The // Creates a new AsyncStreamReader for the given stream. The
// character encoding is set by encoding and the buffer size, // character encoding is set by encoding and the buffer size,
// in number of 16-bit characters, is set by bufferSize. // in number of 16-bit characters, is set by bufferSize.
@ -96,15 +96,19 @@ internal sealed class AsyncStreamReader : IDisposable
{ {
try try
{ {
var bytesRead = await _stream.ReadAsync(new Memory<byte>(_byteBuffer), _cts.Token).ConfigureAwait(false); var bytesRead = await _stream
.ReadAsync(new Memory<byte>(_byteBuffer), _cts.Token)
.ConfigureAwait(false);
if (bytesRead == 0) if (bytesRead == 0)
break; break;
var charLen = _decoder.GetChars(_byteBuffer, 0, bytesRead, _charBuffer, 0); var charLen = _decoder.GetChars(_byteBuffer, 0, bytesRead, _charBuffer, 0);
Debug.WriteLine($"AsyncStreamReader - Read {charLen} chars: " + Debug.WriteLine(
$"{new string(_charBuffer, 0, charLen).ToRepr()}"); $"AsyncStreamReader - Read {charLen} chars: "
+ $"{new string(_charBuffer, 0, charLen).ToRepr()}"
);
_sb!.Append(_charBuffer, 0, charLen); _sb!.Append(_charBuffer, 0, charLen);
MoveLinesFromStringBuilderToMessageQueue(); MoveLinesFromStringBuilderToMessageQueue();
} }
@ -140,44 +144,49 @@ internal sealed class AsyncStreamReader : IDisposable
var remaining = _sb.ToString(); var remaining = _sb.ToString();
_messageQueue.Enqueue(remaining); _messageQueue.Enqueue(remaining);
_sb.Length = 0; _sb.Length = 0;
Debug.WriteLine($"AsyncStreamReader - Reached EOF, sent remaining buffer: {remaining}"); Debug.WriteLine(
$"AsyncStreamReader - Reached EOF, sent remaining buffer: {remaining}"
);
} }
_messageQueue.Enqueue(null); _messageQueue.Enqueue(null);
} }
FlushMessageQueue(rethrowInNewThread: true); FlushMessageQueue(rethrowInNewThread: true);
} }
// Send remaining buffer // Send remaining buffer
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private void SendRemainingBuffer() private void SendRemainingBuffer()
{ {
lock (_messageQueue) lock (_messageQueue)
{ {
if (_sb!.Length == 0) return; if (_sb!.Length == 0)
return;
_messageQueue.Enqueue(_sb.ToString()); _messageQueue.Enqueue(_sb.ToString());
_sb.Length = 0; _sb.Length = 0;
} }
} }
// Send remaining buffer from index // Send remaining buffer from index
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private void SendRemainingBuffer(int startIndex) private void SendRemainingBuffer(int startIndex)
{ {
lock (_messageQueue) lock (_messageQueue)
{ {
if (_sb!.Length == 0) return; if (_sb!.Length == 0)
return;
_messageQueue.Enqueue(_sb.ToString(startIndex, _sb.Length - startIndex)); _messageQueue.Enqueue(_sb.ToString(startIndex, _sb.Length - startIndex));
_sb.Length = 0; _sb.Length = 0;
} }
} }
// Sends a message to the queue if not null or empty // Sends a message to the queue if not null or empty
private void SendToQueue(string? message) private void SendToQueue(string? message)
{ {
if (string.IsNullOrEmpty(message)) return; if (string.IsNullOrEmpty(message))
return;
lock (_messageQueue) lock (_messageQueue)
{ {
_messageQueue.Enqueue(message); _messageQueue.Enqueue(message);
@ -195,14 +204,6 @@ internal sealed class AsyncStreamReader : IDisposable
var currentIndex = _currentLinePos; var currentIndex = _currentLinePos;
var lineStart = 0; var lineStart = 0;
var len = _sb!.Length; var len = _sb!.Length;
// If flagged, send next buffer immediately
if (_sendNextBufferImmediately)
{
SendRemainingBuffer();
_sendNextBufferImmediately = false;
return;
}
// skip a beginning '\n' character of new block if last block ended // skip a beginning '\n' character of new block if last block ended
// with '\r' // with '\r'
@ -225,7 +226,7 @@ internal sealed class AsyncStreamReader : IDisposable
// Include the '\n' as part of line. // Include the '\n' as part of line.
var line = _sb.ToString(lineStart, currentIndex - lineStart + 1); var line = _sb.ToString(lineStart, currentIndex - lineStart + 1);
lineStart = currentIndex + 1; lineStart = currentIndex + 1;
lock (_messageQueue) lock (_messageQueue)
{ {
_messageQueue.Enqueue(line); _messageQueue.Enqueue(line);
@ -255,8 +256,9 @@ internal sealed class AsyncStreamReader : IDisposable
{ {
// Send buffer up to this point, not including \r // Send buffer up to this point, not including \r
// But skip if there's no content // But skip if there's no content
if (currentIndex == lineStart) break; if (currentIndex == lineStart)
break;
var line = _sb.ToString(lineStart, currentIndex - lineStart); var line = _sb.ToString(lineStart, currentIndex - lineStart);
lock (_messageQueue) lock (_messageQueue)
{ {
@ -277,7 +279,7 @@ internal sealed class AsyncStreamReader : IDisposable
{ {
searchIndex++; searchIndex++;
} }
// If we found StEscape, we have a complete APC message // If we found StEscape, we have a complete APC message
if (searchIndex < len) if (searchIndex < len)
{ {
@ -294,9 +296,13 @@ internal sealed class AsyncStreamReader : IDisposable
// lineStart = searchIndex + 1; // lineStart = searchIndex + 1;
currentIndex = searchIndex; currentIndex = searchIndex;
var remainingStart = currentIndex + 1; var remainingStart = currentIndex + 1;
var remainingStr = var remainingStr = _sb.ToString(
_sb.ToString(remainingStart, _sb.Length - remainingStart); remainingStart,
Debug.WriteLine($"AsyncStreamReader - Sending remaining buffer: '{remainingStr}'"); _sb.Length - remainingStart
);
Debug.WriteLine(
$"AsyncStreamReader - Sending remaining buffer: '{remainingStr}'"
);
// Send the rest of the buffer immediately // Send the rest of the buffer immediately
SendRemainingBuffer(currentIndex + 1); SendRemainingBuffer(currentIndex + 1);
return; return;
@ -313,19 +319,21 @@ internal sealed class AsyncStreamReader : IDisposable
SendToQueue(line); SendToQueue(line);
// Set line start to current index // Set line start to current index
lineStart = currentIndex; lineStart = currentIndex;
// Look ahead and match the escape sequence // Look ahead and match the escape sequence
var remaining = _sb.ToString(currentIndex, len - currentIndex); var remaining = _sb.ToString(currentIndex, len - currentIndex);
var result = AnsiParser.AnsiEscapeSequenceRegex().Match(remaining); var result = AnsiParser.AnsiEscapeSequenceRegex().Match(remaining);
// If we found a match, send the escape sequence match, and move forward // If we found a match, send the escape sequence match, and move forward
if (result.Success) if (result.Success)
{ {
var escapeSequence = result.Value; var escapeSequence = result.Value;
SendToQueue(escapeSequence); SendToQueue(escapeSequence);
Debug.WriteLine($"AsyncStreamReader - Sent Ansi escape sequence: {escapeSequence.ToRepr()}"); Debug.WriteLine(
$"AsyncStreamReader - Sent Ansi escape sequence: {escapeSequence.ToRepr()}"
);
// Advance currentIndex and lineStart to end of escape sequence // Advance currentIndex and lineStart to end of escape sequence
// minus 1 since we will increment currentIndex at the end of the loop // minus 1 since we will increment currentIndex at the end of the loop
lineStart = currentIndex + escapeSequence.Length; lineStart = currentIndex + escapeSequence.Length;
@ -333,7 +341,9 @@ internal sealed class AsyncStreamReader : IDisposable
} }
else else
{ {
Debug.WriteLine($"AsyncStreamReader - No match for Ansi escape sequence: {remaining.ToRepr()}"); Debug.WriteLine(
$"AsyncStreamReader - No match for Ansi escape sequence: {remaining.ToRepr()}"
);
} }
break; break;
@ -345,7 +355,15 @@ internal sealed class AsyncStreamReader : IDisposable
{ {
_bLastCarriageReturn = true; _bLastCarriageReturn = true;
} }
// If flagged, send remaining buffer immediately
if (_sendNextBufferImmediately)
{
SendRemainingBuffer();
_sendNextBufferImmediately = false;
return;
}
// Keep the rest characters which can't form a new line in string builder. // Keep the rest characters which can't form a new line in string builder.
if (lineStart < len) if (lineStart < len)
{ {
@ -404,7 +422,10 @@ internal sealed class AsyncStreamReader : IDisposable
// Otherwise, let the exception propagate. // Otherwise, let the exception propagate.
if (rethrowInNewThread) if (rethrowInNewThread)
{ {
ThreadPool.QueueUserWorkItem(edi => ((ExceptionDispatchInfo)edi!).Throw(), ExceptionDispatchInfo.Capture(e)); ThreadPool.QueueUserWorkItem(
edi => ((ExceptionDispatchInfo)edi!).Throw(),
ExceptionDispatchInfo.Capture(e)
);
return true; return true;
} }
throw; throw;

Loading…
Cancel
Save