Browse Source

More console ansi parse stuff

pull/55/head
Ionite 1 year ago
parent
commit
3487e9833a
No known key found for this signature in database
  1. 31
      StabilityMatrix.Avalonia/ViewModels/ConsoleViewModel.cs
  2. 92
      StabilityMatrix.Core/Helper/ReaderWriterLockAdvanced.cs
  3. 76
      StabilityMatrix.Core/Processes/AsyncStreamReader.cs
  4. 5
      StabilityMatrix.Core/Processes/ProcessOutput.cs

31
StabilityMatrix.Avalonia/ViewModels/ConsoleViewModel.cs

@ -156,11 +156,13 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable
// Thrown when buffer is completed, convert to OperationCanceledException
throw new OperationCanceledException("Update buffer completed", e);
}
Debug.WriteLine($"Processing: (Text = {output.Text.ToRepr()}, " +
var outputType = output.IsStdErr ? "stderr" : "stdout";
Debug.WriteLine($"Processing: [{outputType}] (Text = {output.Text.ToRepr()}, " +
$"Raw = {output.RawText?.ToRepr()}, " +
$"CarriageReturn = {output.CarriageReturn}, " +
$"CursorUp = {output.CursorUp})");
$"CursorUp = {output.CursorUp}, " +
$"AnsiCommand = {output.AnsiCommand})");
// Link the cancellation token to the write cursor lock timeout
var linkedCt = CancellationTokenSource
@ -222,6 +224,12 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable
}
else
{
// Also remove everything on current line
// We'll temporarily do this for now to fix progress
var lineEndOffset = currentLine.EndOffset;
var lineLength = lineEndOffset - lineStartOffset;
Document.Remove(lineStartOffset, lineLength);
Debug.WriteLine($"Moving cursor to start for carriage return " +
$"({writeCursor} -> {lineStartOffset})");
writeCursor = lineStartOffset;
@ -270,7 +278,10 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable
// Insert the text
Debug.WriteLine($"Erasing line {currentLine.LineNumber}: (length = {currentLine.Length})");
Document.Replace(currentLine.Offset, currentLine.Length, spaces);
using (Document.RunUpdate())
{
Document.Replace(currentLine.Offset, currentLine.Length, spaces);
}
}
DebugPrintDocument();
@ -297,7 +308,7 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable
// Split text into lines
var lines = text.Split(Environment.NewLine).ToList();
foreach (var (i, lineText) in lines.SkipLast(1).Enumerate())
foreach (var lineText in lines.SkipLast(1))
{
// Insert text
DirectWriteToConsole(lineText);
@ -390,7 +401,15 @@ public partial class ConsoleViewModel : ObservableObject, IDisposable
var linePadding = numberPadding + 3 + cursorLineOffset;
var cursorLineArrow = new string('~', linePadding) + $"^ ({writeCursor})";
lines.Insert(cursorLine.LineNumber, cursorLineArrow);
// If more than line count, append to end
if (cursorLine.LineNumber >= lines.Count)
{
lines.Add(cursorLineArrow);
}
else
{
lines.Insert(cursorLine.LineNumber, cursorLineArrow);
}
var textWithCursor = string.Join(Environment.NewLine, lines);

92
StabilityMatrix.Core/Helper/ReaderWriterLockAdvanced.cs

@ -0,0 +1,92 @@
namespace StabilityMatrix.Core.Helper;
/// <summary>
/// Extended <see cref="ReaderWriterLockSlim"/> with support for disposal pattern.
/// </summary>
public class ReaderWriterLockAdvanced : ReaderWriterLockSlim
{
public ReaderWriterLockAdvanced()
{
}
public ReaderWriterLockAdvanced(LockRecursionPolicy recursionPolicy) : base(recursionPolicy)
{
}
public DisposableLock EnterReadContext(TimeSpan timeout = default)
{
if (!TryEnterReadLock(timeout))
{
throw new TimeoutException("Timeout waiting for read lock");
}
return new DisposableLock(this, LockType.Read);
}
public DisposableLock EnterWriteContext(TimeSpan timeout = default)
{
if (!TryEnterWriteLock(timeout))
{
throw new TimeoutException("Timeout waiting for write lock");
}
return new DisposableLock(this, LockType.Write);
}
public DisposableLock EnterUpgradeableReadContext(TimeSpan timeout = default)
{
if (!TryEnterUpgradeableReadLock(timeout))
{
throw new TimeoutException("Timeout waiting for upgradeable read lock");
}
return new DisposableLock(this, LockType.UpgradeableRead);
}
}
/// <summary>
/// Wrapper for disposable lock
/// </summary>
public class DisposableLock : IDisposable
{
private readonly ReaderWriterLockAdvanced readerWriterLock;
private readonly LockType lockType;
public DisposableLock(ReaderWriterLockAdvanced @lock, LockType lockType)
{
readerWriterLock = @lock;
this.lockType = lockType;
}
public DisposableLock UpgradeToWrite(TimeSpan timeout = default)
{
if (lockType != LockType.UpgradeableRead)
{
throw new InvalidOperationException("Can only upgrade from upgradeable read lock");
}
return readerWriterLock.EnterWriteContext(timeout);
}
public void Dispose()
{
switch (lockType)
{
case LockType.Read:
readerWriterLock.ExitReadLock();
break;
case LockType.Write:
readerWriterLock.ExitWriteLock();
break;
case LockType.UpgradeableRead:
readerWriterLock.ExitUpgradeableReadLock();
break;
default:
throw new ArgumentOutOfRangeException();
}
GC.SuppressFinalize(this);
}
}
public enum LockType
{
Read,
Write,
UpgradeableRead
}

76
StabilityMatrix.Core/Processes/AsyncStreamReader.cs

@ -137,8 +137,10 @@ internal sealed class AsyncStreamReader : IDisposable
{
if (_sb!.Length != 0)
{
_messageQueue.Enqueue(_sb.ToString());
var remaining = _sb.ToString();
_messageQueue.Enqueue(remaining);
_sb.Length = 0;
Debug.WriteLine($"AsyncStreamReader - Reached EOF, sent remaining buffer: {remaining}");
}
_messageQueue.Enqueue(null);
}
@ -171,6 +173,16 @@ internal sealed class AsyncStreamReader : IDisposable
_sb.Length = 0;
}
}
// Sends a message to the queue if not null or empty
private void SendToQueue(string? message)
{
if (string.IsNullOrEmpty(message)) return;
lock (_messageQueue)
{
_messageQueue.Enqueue(message);
}
}
// Read lines stored in StringBuilder and the buffer we just read into.
// A line is defined as a sequence of characters followed by
@ -184,10 +196,6 @@ internal sealed class AsyncStreamReader : IDisposable
var lineStart = 0;
var len = _sb!.Length;
// Flag for last index of '/r', by end of processing
// If this is higher than the index sent, we send the remaining buffer
var lastCarriageReturnIndex = -1;
// If flagged, send next buffer immediately
if (_sendNextBufferImmediately)
{
@ -195,15 +203,6 @@ internal sealed class AsyncStreamReader : IDisposable
_sendNextBufferImmediately = false;
return;
}
// If buffer starts with '\r' not followed by '\n', we sent this immediately
// For progress bars
// But only if no ansi escapes, otherwise handled by ansi block
/*if (len > 0 && _sb[0] == '\r' && (len == 1 || _sb[1] != '\n'))
{
SendRemainingBuffer();
return;
}*/
// skip a beginning '\n' character of new block if last block ended
// with '\r'
@ -231,42 +230,37 @@ internal sealed class AsyncStreamReader : IDisposable
{
_messageQueue.Enqueue(line);
}
break;
}
// \r\n - Windows
// \r alone is parsed as carriage return
case '\r':
{
// when next char is \n, linebreak
var nextIndex = currentIndex + 1;
if (nextIndex < len && _sb[nextIndex] == '\n')
// when next char is \n, we found \r\n - linebreak
if (currentIndex + 1 < len && _sb[currentIndex + 1] == '\n')
{
// Include the '\r\n' as part of line.
var line = _sb.ToString(lineStart, currentIndex - lineStart + 2);
// Advance 2 chars for \r\n
lineStart = currentIndex + 2;
currentIndex++;
lock (_messageQueue)
{
_messageQueue.Enqueue(line);
}
// Advance 2 chars for \r\n
lineStart = currentIndex + 2;
// Increment one extra plus the end of the loop increment
currentIndex++;
}
else
{
// Set flag to indicate we've seen a \r
lastCarriageReturnIndex = currentIndex;
// Send buffer up to this point, not including \r
var line = _sb.ToString(lineStart, currentIndex - lineStart);
lineStart = currentIndex;
currentIndex++;
lock (_messageQueue)
{
_messageQueue.Enqueue(line);
}
// Set line start to current index
lineStart = currentIndex;
}
break;
}
@ -313,28 +307,26 @@ internal sealed class AsyncStreamReader : IDisposable
{
// Unlike '\n', this char is not included in the line
var line = _sb.ToString(lineStart, currentIndex - lineStart);
SendToQueue(line);
// Set line start to current index
lineStart = currentIndex;
lock (_messageQueue)
{
_messageQueue.Enqueue(line);
}
// Look ahead and match the escape sequence
var remaining = _sb.ToString(currentIndex, _sb.Length - currentIndex);
var remaining = _sb.ToString(currentIndex, len - currentIndex);
var result = AnsiParser.AnsiEscapeSequenceRegex().Match(remaining);
// If we found a match, send the escape sequence match, and move forward
if (result.Success)
{
var escapeSequence = result.Value;
SendToQueue(escapeSequence);
Debug.WriteLine($"AsyncStreamReader - Sent Ansi escape sequence: {escapeSequence.ToRepr()}");
lock (_messageQueue)
{
_messageQueue.Enqueue(escapeSequence);
}
// Advance currentIndex and lineStart to end of escape sequence
currentIndex += escapeSequence.Length;
lineStart = currentIndex;
// minus 1 since we will increment currentIndex at the end of the loop
lineStart = currentIndex + escapeSequence.Length;
currentIndex = lineStart - 1;
}
else
{
@ -350,12 +342,6 @@ internal sealed class AsyncStreamReader : IDisposable
{
_bLastCarriageReturn = true;
}
// If we found a carriage return, send the remaining buffer
if (lastCarriageReturnIndex > -1)
{
SendRemainingBuffer(lineStart);
return;
}
// Keep the rest characters which can't form a new line in string builder.
if (lineStart < len)

5
StabilityMatrix.Core/Processes/ProcessOutput.cs

@ -65,6 +65,7 @@ public readonly record struct ProcessOutput
// Parse APC message
if (ApcParser.TryParse(text, out var message))
{
Debug.WriteLine("Created new APC ProcessOutput");
// Override and return
return new ProcessOutput
{
@ -142,7 +143,7 @@ public readonly record struct ProcessOutput
text = AnsiParser.AnsiEscapeSequenceRegex().Replace(text, "");
}
return new ProcessOutput
var output = new ProcessOutput
{
RawText = originalText,
Text = text,
@ -151,5 +152,7 @@ public readonly record struct ProcessOutput
CursorUp = cursorUp,
AnsiCommand = ansiCommands,
};
Debug.WriteLine($"Created new ProcessOutput from {originalText.ToRepr()}: {output.ToString()!.ToRepr()}");
return output;
}
}

Loading…
Cancel
Save