Improve exception diagnostics for stdio client (#376)

* Improve exception diagnostics for stdio client

Track the last several lines of stderr and include those in an exception created as part of CleanupAsync if the server process has already exited. This is based on an assumption that the server should never exit prior to CleanupAsync being called.

* Address feedback and fix test failure
This commit is contained in:
Stephen Toub 2025-05-14 10:46:49 -04:00 • committed by GitHub
commit c11c634945
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -53,17 +53,12 @@ internal class StreamClientSessionTransport : TransportBase
_readTask = readTask.Unwrap();
readTask.Start();
SetConnected(true);
SetConnected();
}
/// <inheritdoc/>
public override async Task SendMessageAsync(JsonRpcMessage message, CancellationToken cancellationToken = default)
{
if (!IsConnected)
{
throw new InvalidOperationException("Transport is not connected");
}
string id = "(no id)";
if (message is JsonRpcMessageWithId messageWithId)
{
@ -82,31 +77,22 @@ internal class StreamClientSessionTransport : TransportBase
catch (Exception ex)
{
LogTransportSendFailed(Name, id, ex);
throw new InvalidOperationException("Failed to send message", ex);
throw new IOException("Failed to send message.", ex);
}
}
/// <inheritdoc/>
/// <summary>
/// Asynchronously releases all resources used by the stream client session transport.
/// </summary>
/// <returns>A task that represents the asynchronous dispose operation.</returns>
/// <remarks>
/// This method cancels ongoing operations and waits for the read task to complete
/// before marking the transport as disconnected. It calls <see cref="CleanupAsync"/>
/// to perform the actual cleanup work.
/// After disposal, the transport can no longer be used to send or receive messages.
/// </remarks>
public override ValueTask DisposeAsync() =>
CleanupAsync(CancellationToken.None);
public override ValueTask DisposeAsync() =>
CleanupAsync(cancellationToken: CancellationToken.None);
private async Task ReadMessagesAsync(CancellationToken cancellationToken)
{
Exception? error = null;
try
{
LogTransportEnteringReadMessagesLoop(Name);
while (!cancellationToken.IsCancellationRequested)
while (true)
{
if (await _serverOutput.ReadLineAsync(cancellationToken).ConfigureAwait(false) is not string line)
{
@ -130,12 +116,13 @@ internal class StreamClientSessionTransport : TransportBase
}
catch (Exception ex)
{
error = ex;
LogTransportReadMessagesFailed(Name, ex);
}
finally
{
_readTask = null;
await CleanupAsync(cancellationToken).ConfigureAwait(false);
await CleanupAsync(error, cancellationToken).ConfigureAwait(false);
}
}
@ -166,7 +153,7 @@ internal class StreamClientSessionTransport : TransportBase
}
}
protected virtual async ValueTask CleanupAsync(CancellationToken cancellationToken)
protected virtual async ValueTask CleanupAsync(Exception? error = null, CancellationToken cancellationToken = default)
{
LogTransportShuttingDown(Name);
@ -191,7 +178,7 @@ internal class StreamClientSessionTransport : TransportBase
}
}
SetConnected(false);
SetDisconnected(error);
LogTransportShutDown(Name);
}
}