3

I am writing a c# console app base on net core 3.1 linux

It was expected to

  • run job async
  • await job end
  • catch the kill signal and do some clean job

here is my demo code:


namespace DeveloperHelper
{
    public class Program
    {
        public static async Task Main(string[] args)
        {
            var http = new SimpleHttpServer();
            var t = http.RunAsync();
            Console.WriteLine("Now after http.RunAsync();");
            AppDomain.CurrentDomain.UnhandledException += (s, e) => {
                var ex = (Exception)e.ExceptionObject;
                Console.WriteLine(ex.ToString());
                Environment.Exit(System.Runtime.InteropServices.Marshal.GetHRForException(ex));
            };
            AppDomain.CurrentDomain.ProcessExit +=  async (s, e) =>
            {
                Console.WriteLine("ProcessExit!");
                await Task.Delay(new TimeSpan(0,0,1));
                Console.WriteLine("ProcessExit! finished");
            };
            await Task.WhenAll(t);
        }
    }
    public class SimpleHttpServer
    {
        private readonly HttpListener _httpListener;
        public SimpleHttpServer()
        {
            _httpListener = new HttpListener();
            _httpListener.Prefixes.Add("http://127.0.0.1:5100/");
        }
        public async Task RunAsync()
        {
            _httpListener.Start();
            while (true)
            {
                Console.WriteLine("Now in  while (true)");
                var context = await _httpListener.GetContextAsync();
                var response = context.Response;

                const string rc = "{\"statusCode\":200, \"data\": true}";
                var rbs = Encoding.UTF8.GetBytes(rc);
                var st = response.OutputStream;

                response.ContentType = "application/json";
                response.StatusCode = 200;

                await st.WriteAsync(rbs, 0, rbs.Length);
                context.Response.Close();
            }
        }
    }
}

expect it will print

Now in  while (true)
Now after http.RunAsync();
ProcessExit!
ProcessExit! finished

but it only output

$ dotnet run
Now in  while (true)
Now after http.RunAsync();
^C%

does the async/await block the kill signal to be watched by eventHandler?

the unexpected exception eventHandler do not have any output too.

is there any signal.signal(signal.SIGTERM, func) in asp.net core?

Panic
  • 325
  • 2
  • 10
  • Does this help? https://learn.microsoft.com/en-us/dotnet/api/system.console.cancelkeypress?view=netcore-3.1 – Andy Aug 05 '20 at 02:48
  • @Andy Thanks, after add `Console.CancelKeyPress += (s,e) => {...} `, ctrl+c will be catch by `Console.CancelKeyPress` and kill signal will be catch by `AppDomain.CurrentDomain.ProcessExit`. but the AppDomain.CurrentDomain.ProcessExit does not await, it only print `ProcessExit!`, the output after await task.delay() does not print – Panic Aug 05 '20 at 03:31
  • I have one side question. Why are you using `HttpListener`? You are using .NET Core, why not use Kestrel and get a full blown web server that works much better? Here is an example of how to set it up: https://stackoverflow.com/a/48343672/1204153 – Andy Aug 05 '20 at 03:35
  • I'll throw together an example of how to get everything to exit gracefully. I have a couple ideas. – Andy Aug 05 '20 at 03:38
  • @Andy using a HttpListener simple open a port and ack request, by this simple impl of a web server, I can register an HTTP check to consul agent,thanks for your advice, I will look at it , do kestrel impl simple than(less code lines than) httplistener? – Panic Aug 05 '20 at 03:39
  • Way simpler -- you don't see `HttpListener` used much anymore because the Kestrel implementation is so well done. – Andy Aug 05 '20 at 03:40
  • @Andy another question, When use kestrel to impl a simple server,do `applicationLifetime.ApplicationStarted/Stopped.Register(() => {}) ` works?.when I use it in asp.net mvc core, it seems works properly – Panic Aug 05 '20 at 03:44
  • @Andy `I have a couple ideas` waiting for you code sincerely – Panic Aug 05 '20 at 03:46

1 Answers1

2

Ok, this may be a tad long winded, but here it goes.

The main issue here is HttpListener.GetContextAsync() does not support cancellation via CancellationToken. So it's tough to cancel this operation in a somewhat graceful manner. What we need to do is "fake" a cancellation.

Stephen Toub is a master in the async/await pattern. Luckily for us he wrote an article entitled How do I cancel non-cancelable async operations?. You can check it out here.

I don't believe in using the AppDomain.CurrentDomain.ProcessExit event. You can read up on why some folks try to avoid it.

I will use the Console.CancelKeyPress event though.

So, in the program file, I have set it up like this:

Program.cs

class Program
{
    private static readonly CancellationTokenSource _cancellationToken =
        new CancellationTokenSource();

    static async Task Main(string[] args)
    {
        var http = new SimpleHttpServer();
        var taskRunHttpServer = http.RunAsync(_cancellationToken.Token);
        Console.WriteLine("Now after http.RunAsync();");

        Console.CancelKeyPress += (s, e) =>
        {
            _cancellationToken.Cancel();
        };

        await taskRunHttpServer;

        Console.WriteLine("Program end");
    }
}

I took your code and added the Console.CancelKeyPress event and added a CancellationTokenSource. I also modified your SimpleHttpServer.RunAsync() method to accept a token from that source:

SimpleHttpServer.cs

public class SimpleHttpServer
{
    private readonly HttpListener _httpListener;
    public SimpleHttpServer()
    {
        _httpListener = new HttpListener();
        _httpListener.Prefixes.Add("http://127.0.0.1:5100/");
    }
    public async Task RunAsync(CancellationToken token)
    {
        try
        {
            _httpListener.Start();
            while (!token.IsCancellationRequested)
            {
                // ...

                var context = await _httpListener.GetContextAsync().
                    WithCancellation(token);
                var response = context.Response;

                // ...
            }
        }
        catch(OperationCanceledException)
        {
            // we are going to ignore this and exit gracefully
        }
    }
}

Instead of looping on true, I now loop on the whether or not the token is signaled as cancelled or not.

Another thing that is quite odd about this is the addition of WithCancellation method to the _httpListener.GetContextAsync() line.

This code is from the Stephen Toub article above. I created a new file that is meant to hold extensions for tasks:

TaskExtensions.cs

public static class TaskExtensions
{
    public static async Task<T> WithCancellation<T>(
        this Task<T> task, CancellationToken cancellationToken)
    {
        var tcs = new TaskCompletionSource<bool>();
        using (cancellationToken.Register(s => ((TaskCompletionSource<bool>)s).TrySetResult(true), tcs))
            if (task != await Task.WhenAny(task, tcs.Task))
                throw new OperationCanceledException(cancellationToken);
        return await task;
    }
}

I won't go in to much detail about how it works because the article above explains it just fine.

Now, when you catch the CTRL+C signal, the token is signaled to cancel which will throw a OperationCanceledException which breaks that loop. We catch it and toss it aside and exit.

If you want to continue to use AppDomain.CurrentDomain.ProcessExit, you can -- your choice.. just add the code inside of Console.CancelKeyPress in to that event.

The program will then exit gracefully... well, as gracefully as it can.

Andy
  • 12,859
  • 5
  • 41
  • 56
  • thanks. the `Console.CancelKeyPress` meant to catch ctrl+c, when the console app run in background with nohup, will need a graceful way to catch kill signal. – Panic Aug 05 '20 at 05:01
  • @Panic -- Check out: https://dejanstojanovic.net/aspnet/2018/june/clean-service-stop-on-linux-with-net-core-21/ – Andy Aug 05 '20 at 05:02
  • and, I appreciate to see the Kestrel implementation of this webserver with simple {"code":200, "data":200 } return – Panic Aug 05 '20 at 05:03