Search code examples
asp.net-coreconsolecancellation-tokengraceful-shutdown

How to correctly handle Ctrl-C or SIGINT in an ASP.NET Core console application?


I have an ASP.NET Core console application, that runs a REST interface and a pool of websockets. When I press Ctrl-C in the console, it shuts down correctly, unless I have a WebSocket connected. That socket is then blocked in ReceiveAsync. ReceiveAsync takes a cancellation token, but I fail to get the 'right' token for this to work.

What I have now is this, but that doesn't work. When I Ctrl-C, it only stops with a timeout:

    public async Task<WebSocketReceiveResult> ReceiveAsync()
    {
        byte[] buffer = new byte[1024];
        var arraysegment = new ArraySegment<byte>(buffer);

        var cancellationTokenSource = new CancellationTokenSource();
        AppDomain.CurrentDomain.ProcessExit += (s, e) =>
        {
            cancellationTokenSource.Cancel();
        };

        return await _websocket.ReceiveAsync(arraysegment, cancellationTokenSource.Token);
    }

The timeout exception that I get is this:

2025-01-07 16:19:14.966 [E] [Restinterface] Request 'GET http://127.0.0.1:5000/ws' failed: 'The connection was aborted because the server is shutting down and request processing didn't complete within the time specified by HostOptions.ShutdownTimeout.', returned HTTP500. This should not happen!
2025-01-07 16:19:14.992 [D] [Restinterface]    at System.IO.Pipelines.Pipe.GetReadResult(ReadResult& result)
   at System.IO.Pipelines.Pipe.GetReadAsyncResult()
   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.Http1UpgradeMessageBody.ReadAsyncInternalAwaited(ValueTask`1 readTask, CancellationToken cancellationToken)
   at System.Runtime.CompilerServices.PoolingAsyncValueTaskMethodBuilder`1.StateMachineBox`1.System.Threading.Tasks.Sources.IValueTaskSource<TResult>.GetResult(Int16 token)
   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpRequestStream.ReadAsyncInternal(Memory`1 destination, CancellationToken cancellationToken)
   at System.Runtime.CompilerServices.PoolingAsyncValueTaskMethodBuilder`1.StateMachineBox`1.System.Threading.Tasks.Sources.IValueTaskSource<TResult>.GetResult(Int16 token)
   at System.IO.Stream.ReadAtLeastAsyncCore(Memory`1 buffer, Int32 minimumBytes, Boolean throwOnEndOfStream, CancellationToken cancellationToken)
   at System.Runtime.CompilerServices.PoolingAsyncValueTaskMethodBuilder`1.StateMachineBox`1.System.Threading.Tasks.Sources.IValueTaskSource<TResult>.GetResult(Int16 token)
   at System.Net.WebSockets.ManagedWebSocket.EnsureBufferContainsAsync(Int32 minimumRequiredBytes, CancellationToken cancellationToken)
   at System.Runtime.CompilerServices.PoolingAsyncValueTaskMethodBuilder`1.StateMachineBox`1.System.Threading.Tasks.Sources.IValueTaskSource.GetResult(Int16 token)
   at System.Net.WebSockets.ManagedWebSocket.ReceiveAsyncPrivate[TResult](Memory`1 payloadBuffer, CancellationToken cancellationToken)
   at System.Runtime.CompilerServices.PoolingAsyncValueTaskMethodBuilder`1.StateMachineBox`1.System.Threading.Tasks.Sources.IValueTaskSource<TResult>.GetResult(Int16 token)
   at System.Threading.Tasks.ValueTask`1.ValueTaskSourceAsTask.<>c.<.cctor>b__4_0(Object state)
--- End of stack trace from previous location ---
   at PtlRestInterface.WebSocketAdapter.ReceiveAsync() in C:\Users\bart-devel\source\repos\ptl\PTLRestInterface\WebSocket.cs:line 50
   at PtlRestInterface.RestInterface.WebSocketLoopAsync(IWebSocket webSocket) in C:\Users\bart-devel\source\repos\ptl\PTLRestInterface\RestHandlers\WebSocketHandler.cs:line 31
   at PtlRestInterface.RestInterface.HandleWebSocketAsync(HttpContext context) in C:\Users\bart-devel\source\repos\ptl\PTLRestInterface\RestHandlers\WebSocketHandler.cs:line 15
   at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
   at PtlRestInterface.RestInterface.<Configure>b__79_0(HttpContext context, Func`1 next) in C:\Users\bart-devel\source\repos\ptl\PTLRestInterface\RestInterface.cs:line 79

I want the ReceiveAsync to gracefully close when I press Ctrl-C on the console (or when sending a SIGINT).


Solution

  • I added this code to the host application to catch the Ctrl-C:

    var applicationLifetime = app.ApplicationServices.GetRequiredService<IHostApplicationLifetime>(); 
    applicationLifetime.ApplicationStopping.Register(() => 
    { 
        CancellationTokenSource.Cancel(); 
    });
    

    I can then explicitly Cancel() the CancellationTokenSource, that I have provided to the WebSocket handler earlier.