Fix StdioServerTransport.DisposeAsync hang (#235)
- It's not just Unix, it's all platforms that can hang - Windows doesn't hang on Ctrl-C because that triggers the end of the Stream, but that doesn't help in IHostApplicationLifetime.StopAsync() scenarios4e9627e311/src/libraries/System.Console/src/System/ConsolePal.Unix.ConsoleStream.cs (L13)4e9627e311/src/libraries/System.Console/src/System/ConsolePal.Windows.cs (L1149)
This commit is contained in:
parent
3e21f3594a
commit
bab74d5895
1 changed files with 114 additions and 8 deletions
|
|
@ -60,7 +60,7 @@ public sealed class StdioServerTransport : StreamServerTransport
|
|||
/// </para>
|
||||
/// </remarks>
|
||||
public StdioServerTransport(string serverName, ILoggerFactory? loggerFactory = null)
|
||||
: base(Console.OpenStandardInput(),
|
||||
: base(new CancellableStdinStream(Console.OpenStandardInput()),
|
||||
new BufferedStream(Console.OpenStandardOutput()),
|
||||
serverName ?? throw new ArgumentNullException(nameof(serverName)),
|
||||
loggerFactory)
|
||||
|
|
@ -73,4 +73,37 @@ public sealed class StdioServerTransport : StreamServerTransport
|
|||
|
||||
return serverOptions.ServerInfo?.Name ?? McpServer.DefaultImplementation.Name;
|
||||
}
|
||||
|
||||
// Neither WindowsConsoleStream nor UnixConsoleStream respect CancellationTokens or cancel any I/O on Dispose.
|
||||
// WindowsConsoleStream will return an EOS on Ctrl-C, but that is not the only reason the shutdownToken may fire.
|
||||
private sealed class CancellableStdinStream(Stream stdinStream) : Stream
|
||||
{
|
||||
public override bool CanRead => true;
|
||||
public override bool CanSeek => false;
|
||||
public override bool CanWrite => false;
|
||||
|
||||
public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
|
||||
=> stdinStream.ReadAsync(buffer, offset, count, cancellationToken).WaitAsync(cancellationToken);
|
||||
|
||||
#if NET
|
||||
public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ValueTask<int> vt = stdinStream.ReadAsync(buffer, cancellationToken);
|
||||
return vt.IsCompletedSuccessfully ? vt : new(vt.AsTask().WaitAsync(cancellationToken));
|
||||
}
|
||||
#endif
|
||||
|
||||
// The McpServer shouldn't call flush on the stdin Stream, but it doesn't need to throw just in case.
|
||||
public override void Flush() { }
|
||||
|
||||
public override long Length => throw new NotSupportedException();
|
||||
|
||||
public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); }
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException();
|
||||
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
|
||||
|
||||
public override void SetLength(long value) => throw new NotSupportedException();
|
||||
public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue