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?

8
  • Does this help? learn.microsoft.com/en-us/dotnet/api/… Commented Aug 5, 2020 at 2: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 Commented Aug 5, 2020 at 3: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: stackoverflow.com/a/48343672/1204153 Commented Aug 5, 2020 at 3:35
  • I'll throw together an example of how to get everything to exit gracefully. I have a couple ideas. Commented Aug 5, 2020 at 3: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? Commented Aug 5, 2020 at 3:39

1 Answer 1

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.

Sign up to request clarification or add additional context in comments.

3 Comments

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.
and, I appreciate to see the Kestrel implementation of this webserver with simple {"code":200, "data":200 } return

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.