Public Access
Replay the live sessions to a renderer that just attached
Each live session gets its credit window reset — the unacknowledged bytes died with the old page, and their acknowledgement is never coming — and its SessionOpened frame again, flagged as a replay so the page can tell a reattach from a genuinely new session. A session whose shell already ended gets nothing: its scrollback lived only in the page that is gone, and a frame implying otherwise would lie. RendererReattached is the seam for what the workspace has no business owning: the font size and the selected tab live in the shell, which re-pushes them from its own subscription.
This commit is contained in:
@@ -8,6 +8,8 @@ internal sealed class FakeShellSession : ISshShellSession
|
||||
private readonly List<byte> written = [];
|
||||
private readonly Lock gate = new();
|
||||
|
||||
private readonly bool blockReads;
|
||||
|
||||
private long remaining;
|
||||
private byte pattern;
|
||||
|
||||
@@ -16,7 +18,19 @@ internal sealed class FakeShellSession : ISshShellSession
|
||||
/// endless producer, which is what a runaway remote process looks like — those sessions are ended
|
||||
/// by disposing the pump rather than by running out of data.
|
||||
/// </param>
|
||||
internal FakeShellSession(long bytesToProduce = 0) => remaining = bytesToProduce;
|
||||
/// <param name="blockReads">
|
||||
/// True for a shell that is open and live but has nothing to say — an idle prompt, rather than either
|
||||
/// end of the "produces bytes" and "hit end of stream" spectrum <paramref name="bytesToProduce"/>
|
||||
/// covers. <see cref="ReadAsync"/> then blocks until cancelled, which is what a real idle SSH channel's
|
||||
/// read does. Exists for tests that need a session whose <c>Run</c> stays live without a background
|
||||
/// read loop racing the test for control of the pump's credit window — see the reattach tests in
|
||||
/// <c>TerminalWorkspaceTests</c>.
|
||||
/// </param>
|
||||
internal FakeShellSession(long bytesToProduce = 0, bool blockReads = false)
|
||||
{
|
||||
remaining = bytesToProduce;
|
||||
this.blockReads = blockReads;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsOpen { get; private set; } = true;
|
||||
@@ -52,6 +66,13 @@ internal sealed class FakeShellSession : ISshShellSession
|
||||
{
|
||||
ReadCount++;
|
||||
|
||||
if (blockReads)
|
||||
{
|
||||
// Never completes on its own. The only way out is the same way a real blocked read ends: the
|
||||
// token being cancelled, which is what disposing the pump does.
|
||||
await Task.Delay(Timeout.InfiniteTimeSpan, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await Task.Yield();
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
@@ -109,7 +130,8 @@ internal sealed class FakeShellSession : ISshShellSession
|
||||
/// workspace is the layer that decides when a session is over, and that decision is what needs a
|
||||
/// connection whose shell can be made to end on cue.
|
||||
/// </remarks>
|
||||
internal sealed class FakeConnectionFactory(long bytesPerShell = long.MaxValue) : ISshConnectionFactory
|
||||
internal sealed class FakeConnectionFactory(long bytesPerShell = long.MaxValue, bool blockShellReads = false)
|
||||
: ISshConnectionFactory
|
||||
{
|
||||
/// <summary>Connections handed out, in order.</summary>
|
||||
internal List<FakeConnection> Connections { get; } = [];
|
||||
@@ -119,7 +141,7 @@ internal sealed class FakeConnectionFactory(long bytesPerShell = long.MaxValue)
|
||||
SshConnectionRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var connection = new FakeConnection(request, bytesPerShell);
|
||||
var connection = new FakeConnection(request, bytesPerShell, blockShellReads);
|
||||
Connections.Add(connection);
|
||||
|
||||
return Task.FromResult<ISshConnection>(connection);
|
||||
@@ -127,7 +149,8 @@ internal sealed class FakeConnectionFactory(long bytesPerShell = long.MaxValue)
|
||||
}
|
||||
|
||||
/// <summary>A connection that opens fake shells and records its own disposal.</summary>
|
||||
internal sealed class FakeConnection(SshConnectionRequest request, long bytesPerShell) : ISshConnection
|
||||
internal sealed class FakeConnection(SshConnectionRequest request, long bytesPerShell, bool blockShellReads = false)
|
||||
: ISshConnection
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public bool IsConnected { get; private set; } = true;
|
||||
@@ -148,7 +171,7 @@ internal sealed class FakeConnection(SshConnectionRequest request, long bytesPer
|
||||
/// <inheritdoc />
|
||||
public Task<ISshShellSession> OpenShellAsync(TerminalSize size, CancellationToken cancellationToken)
|
||||
{
|
||||
Shell = new FakeShellSession(bytesPerShell);
|
||||
Shell = new FakeShellSession(bytesPerShell, blockShellReads);
|
||||
|
||||
return Task.FromResult<ISshShellSession>(Shell);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user