Fix terminal reconnect output pump lifetime

This commit is contained in:
sladro 2026-04-04 09:47:57 +08:00
parent f2282b8619
commit 4fcbb07fdc
2 changed files with 44 additions and 1 deletions

View File

@ -22,6 +22,7 @@ internal sealed class HelperBackedConPtySession : IConPtySession
private StreamWriter? _commandWriter; private StreamWriter? _commandWriter;
private StreamReader? _outputReader; private StreamReader? _outputReader;
private Task? _outputPumpTask; private Task? _outputPumpTask;
private readonly CancellationTokenSource _lifetime = new();
private bool _started; private bool _started;
private bool _disposed; private bool _disposed;
@ -104,7 +105,7 @@ internal sealed class HelperBackedConPtySession : IConPtySession
throw new InvalidOperationException($"Unexpected ConPTY helper startup handshake: {handshake}"); throw new InvalidOperationException($"Unexpected ConPTY helper startup handshake: {handshake}");
} }
_outputPumpTask = Task.Run(() => PumpOutputAsync(_outputReader, cancellationToken), cancellationToken); _outputPumpTask = Task.Run(() => PumpOutputAsync(_outputReader, _lifetime.Token), CancellationToken.None);
_started = true; _started = true;
} }
@ -149,6 +150,7 @@ internal sealed class HelperBackedConPtySession : IConPtySession
} }
_disposed = true; _disposed = true;
_lifetime.Cancel();
if (_commandWriter is not null) if (_commandWriter is not null)
{ {
@ -182,6 +184,7 @@ internal sealed class HelperBackedConPtySession : IConPtySession
_commandPipe?.Dispose(); _commandPipe?.Dispose();
_outputPipe?.Dispose(); _outputPipe?.Dispose();
_helperProcess?.Dispose(); _helperProcess?.Dispose();
_lifetime.Dispose();
} }
private async Task PumpOutputAsync(StreamReader reader, CancellationToken cancellationToken) private async Task PumpOutputAsync(StreamReader reader, CancellationToken cancellationToken)

View File

@ -44,6 +44,46 @@ public sealed class TerminalSmokeCheckTests
} }
} }
[Fact]
public async Task BuiltAgentExe_Reconnects_Existing_Terminal_And_Accepts_Input()
{
if (!OperatingSystem.IsWindows())
{
return;
}
await using var fixture = new BuiltAgentFixture();
await fixture.StartAsync();
try
{
var session = await fixture.CreateSessionAsync("smoke-reconnect");
using (var firstSocket = await fixture.ConnectTerminalAsync(session.SessionId))
{
_ = await fixture.ReceiveTextAsync(firstSocket, TimeSpan.FromSeconds(20));
await fixture.SendTextAsync(firstSocket, JsonSerializer.Serialize(new { type = "input", input = "Write-Output first\r" }));
_ = await fixture.ReceiveTextContainingAsync(firstSocket, "first", TimeSpan.FromSeconds(20));
}
using var secondSocket = await fixture.ConnectTerminalAsync(session.SessionId);
var attached = await fixture.ReceiveTextAsync(secondSocket, TimeSpan.FromSeconds(20));
var attachedPayload = JsonSerializer.Deserialize<TerminalAttachResponse>(attached, new JsonSerializerOptions(JsonSerializerDefaults.Web));
Assert.NotNull(attachedPayload);
Assert.Equal("attached", attachedPayload!.Type);
Assert.Equal(session.SessionId, attachedPayload.SessionId);
await fixture.SendTextAsync(secondSocket, JsonSerializer.Serialize(new { type = "input", input = "Write-Output second\r" }));
var output = await fixture.ReceiveTextContainingAsync(secondSocket, "second", TimeSpan.FromSeconds(20));
Assert.Contains("second", output, StringComparison.OrdinalIgnoreCase);
}
catch (Exception ex)
{
throw new InvalidOperationException($"{ex.Message}{Environment.NewLine}{fixture.GetDiagnostics()}", ex);
}
}
private sealed class BuiltAgentFixture : IAsyncDisposable private sealed class BuiltAgentFixture : IAsyncDisposable
{ {
private readonly string _projectRoot; private readonly string _projectRoot;