【问题标题】:how to catch exit signal in asp.net core on linux?如何在 Linux 上的 asp.net 核心中捕获退出信号?
【发布时间】:2020-08-04 14:54:31
【问题描述】:

我正在编写一个基于 net core 3.1 linux 的 c# 控制台应用程序

预计会

  • 异步运行作业
  • 等待作业结束
  • 捕捉终止信号并做一些干净的工作

这是我的演示代码:


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();
            }
        }
    }
}

希望它会打印

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

但它只输出

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

async/await 是否阻塞了 eventHandler 监视的 kill 信号?

意外异常事件处理程序也没有任何输出。

asp.net core 中有signal.signal(signal.SIGTERM, func) 吗?

【问题讨论】:

  • @Andy 谢谢,添加Console.CancelKeyPress += (s,e) => {...} 后, ctrl+c 将被Console.CancelKeyPress 捕获,而kill 信号将被AppDomain.CurrentDomain.ProcessExit 捕获。但是 AppDomain.CurrentDomain.ProcessExit 不等待,它只打印ProcessExit!,等待 task.delay() 之后的输出不打印
  • 我有一个侧面问题。你为什么使用HttpListener?您正在使用 .NET Core,为什么不使用 Kestrel 并获得一个运行得更好的完整 Web 服务器?以下是如何设置的示例:stackoverflow.com/a/48343672/1204153
  • 我将整理一个示例,说明如何让所有内容优雅地退出。我有几个想法。
  • @Andy 使用 HttpListener 简单地打开一个端口并确认请求,通过这个简单的网络服务器实现,我可以向 consul 代理注册一个 HTTP 检查,谢谢你的建议,我会看看, kestrel impl 是否比 httplistener 简单(代码行数少于)?

标签: c# .net-core async-await


【解决方案1】:

好的,这可能有点啰嗦,但就这样吧。

这里的主要问题是HttpListener.GetContextAsync() 不支持通过CancellationToken 取消。所以很难以一种有点优雅的方式取消这个操作。我们需要做的是“假”取消。

Stephen Toub 是 async/await 模式的大师。幸运的是,他写了一篇题为如何取消不可取消的异步操作?的文章。您可以查看here

我不相信使用AppDomain.CurrentDomain.ProcessExit 事件。你可以阅读为什么有些人试图避免它。

不过我会使用Console.CancelKeyPress 事件。

所以,在程序文件中,我是这样设置的:

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");
    }
}

我获取了您的代码并添加了Console.CancelKeyPress 事件并添加了CancellationTokenSource。我还修改了您的 SimpleHttpServer.RunAsync() 方法以接受来自该来源的令牌:

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
        }
    }
}

我现在不是在true 上循环,而是在令牌是否被通知为已取消。

另外一件很奇怪的事情是在_httpListener.GetContextAsync() 行中添加了WithCancellation 方法。

此代码来自上面的 Stephen Toub 文章。我创建了一个用于保存任务扩展的新文件:

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;
    }
}

我不会详细介绍它的工作原理,因为上面的文章解释得很好。

现在,当您捕捉到 CTRL+C 信号时,令牌会发出取消信号,这将引发 OperationCanceledException 中断该循环。我们抓住它,把它扔到一边然后退出。

如果您想继续使用AppDomain.CurrentDomain.ProcessExit,您可以 -- 您的选择.. 只需将Console.CancelKeyPress 内的代码添加到该事件中。

然后程序将优雅地退出......好吧,尽可能优雅地退出。

【讨论】:

  • 谢谢。 Console.CancelKeyPress 是为了捕捉 ctrl+c,当控制台应用程序在后台使用 nohup 运行时,需要一种优雅的方式来捕捉 kill 信号。
  • 而且,我很高兴看到这个网络服务器的 Kestrel 实现具有简单的 {"code":200, "data":200 } return
猜你喜欢
  • 2019-10-29
  • 1970-01-01
  • 2021-07-24
  • 1970-01-01
  • 2023-01-29
  • 2013-09-27
  • 2022-08-16
  • 1970-01-01
  • 2013-05-08
相关资源
最近更新 更多