Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 36 additions & 6 deletions src/OneWare.Terminal/Provider/PseudoTerminalConnection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ namespace OneWare.Terminal.Provider;

public class PseudoTerminalConnection(IPseudoTerminal terminal) : IConnection, IOutputFilter, IOutputSuppressor, IDisposable
{
private const int TolerantSuppressionPrefixLength = 4;
private CancellationTokenSource? _cancellationSource;
private readonly object _suppressLock = new();
private readonly Queue<byte[]> _suppressQueue = new();
private readonly List<byte> _pendingSuppression = new();
private byte[]? _activeSuppression;
private int _matchedSuppressionLength;

public bool IsConnected { get; private set; }

Expand Down Expand Up @@ -100,6 +102,7 @@ public byte[] FilterOutput(byte[] data)
{
_activeSuppression = _suppressQueue.Dequeue();
_pendingSuppression.Clear();
_matchedSuppressionLength = 0;
}

if (_activeSuppression == null)
Expand All @@ -109,15 +112,29 @@ public byte[] FilterOutput(byte[] data)
}

var b = data[index++];
var pendingIndex = _pendingSuppression.Count;
if (b == _activeSuppression[_matchedSuppressionLength])
{
_pendingSuppression.Add(b);
_matchedSuppressionLength++;
if (_matchedSuppressionLength == _activeSuppression.Length)
{
ResetActiveSuppression();
}

continue;
}

if (b == _activeSuppression[pendingIndex])
if (_matchedSuppressionLength >=
Math.Min(TolerantSuppressionPrefixLength, _activeSuppression.Length))
{
// Interactive shells may insert cursor movement and redraw bytes while
// echoing a command that wraps. Keep matching the requested sequence as a
// subsequence so those terminal-control bytes do not expose the command.
_pendingSuppression.Add(b);
if (_pendingSuppression.Count == _activeSuppression.Length)
if (b == (byte)'\n')
{
_activeSuppression = null;
_pendingSuppression.Clear();
output.AddRange(_pendingSuppression);
ResetActiveSuppression();
}

continue;
Expand All @@ -127,11 +144,17 @@ public byte[] FilterOutput(byte[] data)
{
output.AddRange(_pendingSuppression);
_pendingSuppression.Clear();
_matchedSuppressionLength = 0;
}

if (_activeSuppression != null && b == _activeSuppression[0])
if (b == _activeSuppression[0])
{
_pendingSuppression.Add(b);
_matchedSuppressionLength = 1;
if (_activeSuppression.Length == 1)
{
ResetActiveSuppression();
}
}
else
{
Expand All @@ -143,6 +166,13 @@ public byte[] FilterOutput(byte[] data)
}
}

private void ResetActiveSuppression()
{
_activeSuppression = null;
_pendingSuppression.Clear();
_matchedSuppressionLength = 0;
}

public void SetTerminalWindowSize(int columns, int rows)
{
terminal.SetSize(columns, rows);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,8 @@ public async Task<TerminalExecutionResult> ExecuteInTerminalAsync(string command
string? workingDirectory = null, bool showInUi = false, TimeSpan? timeout = null,
IProgress<string>? outputProgress = null, CancellationToken cancellationToken = default)
{
_mainDockService.Show<ITerminalManagerService>();
if (showInUi)
_mainDockService.Show<ITerminalManagerService>();

var tab = AcquireAutomationTab(id, workingDirectory, showInUi);
try
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using System.Text;
using OneWare.Terminal.Provider;
using Xunit;

namespace OneWare.Studio.Desktop.UnitTests;

public class PseudoTerminalConnectionTests
{
private const string MarkerCommand =
"__ow_exit=$?; printf '\\033[1A\\r\\033[2K\\033]9;OW_DONE:0123456789abcdef:%s\\007' \"$__ow_exit\"";

[Fact]
public void FilterOutput_SuppressesSequenceAcrossChunks()
{
var connection = new PseudoTerminalConnection(null!);
var marker = Encoding.ASCII.GetBytes(MarkerCommand);
connection.SuppressOutput(marker);

var first = connection.FilterOutput(Encoding.ASCII.GetBytes($"before{MarkerCommand[..30]}"));
var second = connection.FilterOutput(Encoding.ASCII.GetBytes($"{MarkerCommand[30..]}after"));

Assert.Equal("before", Encoding.ASCII.GetString(first));
Assert.Equal("after", Encoding.ASCII.GetString(second));
}

[Fact]
public void FilterOutput_SuppressesWrappedReadlineEcho()
{
var connection = new PseudoTerminalConnection(null!);
connection.SuppressOutput(Encoding.ASCII.GetBytes(MarkerCommand));

var wrappedEcho = MarkerCommand
.Replace("\\033[1A", "\\\\\r\\033[1A")
.Replace("$__ow_exit\"", "$__ow_ex\rxit\"");
var first = connection.FilterOutput(Encoding.ASCII.GetBytes($"prompt{wrappedEcho[..48]}"));
var second = connection.FilterOutput(Encoding.ASCII.GetBytes(wrappedEcho[48..]));

Assert.Equal("prompt", Encoding.ASCII.GetString(first));
Assert.Empty(second);
}

[Fact]
public void FilterOutput_ReleasesIncompleteSuppressionAtLineEnd()
{
var connection = new PseudoTerminalConnection(null!);
connection.SuppressOutput(Encoding.ASCII.GetBytes(MarkerCommand));

var output = connection.FilterOutput(Encoding.ASCII.GetBytes("__ow_broken\r\nnext"));

Assert.Equal("__ow_broken\r\nnext", Encoding.ASCII.GetString(output));
}
}
Loading