簡體   English   中英

如果自定義配置設置在ASP.NET Core 2應用程序啟動過程中不存在/無效(托管在Windows服務中),如何正常失敗?

[英]How to fail gracefully if custom configuration settings are not present/valid during ASP.NET Core 2 application startup (hosted in a Windows Service)?

我正在使用Windows服務中托管的ASP.NET Core 2(針對.NET Framework 4.6.1)實現Web API。

我從在Windows Service托管ASP.NET Core應用程序中的示例開始,但是在實現以下附加要求時遇到了麻煩:

  • Windows服務(或它承載的Web API)啟動或重新啟動時,我想讀取一些配置(碰巧在注冊表中,但可以在任何地方)。

    • 如果此配置丟失或無效,則Web API / Windows服務不應啟動(如果已經啟動則應停止),應該記錄一些錯誤消息,並且最好向服務控制管理器發出一些故障信號(例如ERROR_BAD_CONFIGURATION = 1610) 。
    • 如果該配置存在並且有效,則Windows Service應該啟動,啟動Web API,並且配置設置應該對Web API的控制器可用(通過依賴注入)。

我不清楚Windows服務/ Web應用程序中讀取和驗證自定義配置的邏輯應該放在哪里。
根據配置邏輯的位置,如果配置無效,則需要采用一種優雅的方法來停止進一步的啟動進度並關閉所有已啟動的內容。
我在ASP.NET或Windows Services框架中看不到任何明顯的鈎子。

我曾考慮過將讀取和驗證邏輯放在以下位置,但是每個位置都有一些問題:

  1. 在Main()中,在調用IWebHost.RunAsService()之前
  2. CustomWebHostService的通知方法中(可能是OnStarting()嗎?)
  3. 在初始化Web API的Startup類( Configure()ConfigureServices() )期間

任何人都可以闡明如何做到這一點嗎?

選項1:在IWebHost.RunAsService()之前的Main()中

這似乎是快速失敗的最干凈的方法,因為如果配置丟失/不正確,則Windows服務甚至都不會啟動。
但是,這意味着當服務控制管理器啟動托管可執行文件時,我們不會(通過IWebHost.RunAsService()IWebHost.RunAsService()注冊,因此SCM返回錯誤:

[SC] StartService FAILED 1053:

The service did not respond to the start or control request in a timely fashion.

理想情況下,服務控制管理器應該知道啟動失敗的原因,然后可以將其記錄到事件日志中。
我認為這是不可能的,除非我們向服務控制管理器注冊,這使我們進入了選項2。

選項2:在Windows Service啟動期間

在Web API的前一個版本中(在WCF中),我將ServiceBase子類化,在ServiceBaseSubclass.OnStart()獲取並驗證了配置,如果該配置無效,請按照What is the中的建議設置this.ExitCode並調用Stop() Windows服務失敗的正確方法? 像這樣:

partial class WebServicesHost : ServiceBase
{
    private ServiceHost _webServicesServiceHost;

    // From https://msdn.microsoft.com/en-us/library/windows/desktop/ms681385(v=vs.85).aspx
    private const int ErrorBadConfiguration = 1610;

    protected override void OnStart(string[] args)
    {
      base.OnStart(args);

      var customConfig = ReadAndValidateCustomConfig();
      if (customConfig != null)
      {
        var webService = new WebService(customConfig);
        _webServicesServiceHost = new ServiceHost(webService);
        _webServicesServiceHost.Open();
      }
      else
      {
        // Configuration is bad, stop the service
        ExitCode = ErrorBadConfiguration;
        Stop();
      }
    }
}

然后,當您使用服務控制管理器啟動和查詢Windows服務時,它會正確報告失敗:

C:\> sc start MyService

SERVICE_NAME: MyService
        TYPE               : 10  WIN32_OWN_PROCESS
        STATE              : 2  START_PENDING
                                (NOT_STOPPABLE, NOT_PAUSABLE, IGNORES_SHUTDOWN)
        WIN32_EXIT_CODE    : 0  (0x0)
        SERVICE_EXIT_CODE  : 0  (0x0)
        CHECKPOINT         : 0x0
        WAIT_HINT          : 0x7d0
        PID                : 10764
        FLAGS              :

C:\> sc query MyService

SERVICE_NAME: MyService
        TYPE               : 10  WIN32_OWN_PROCESS
        STATE              : 1  STOPPED            <-- Service is stopped
        WIN32_EXIT_CODE    : 1610  (0x64a)         <-- and reports the reason for stopping
        SERVICE_EXIT_CODE  : 0  (0x0)
        CHECKPOINT         : 0x0
        WAIT_HINT          : 0x0

在ASP.NET Core 2中,如果我將WebHostService子類WebHostService (如在Windows Service托管ASP.NET Core應用程序中所述 ),那么似乎僅存在用於通知Windows Service的啟動工作流進度的鈎子(例如OnStarting()OnStarted() ),但沒有提及如何安全地停止服務。

查看WebHostService源代碼並反編譯ServiceBase使我認為,從CustomWebHostService.OnStarting()調用ServiceBase.Stop()是一個非常糟糕的主意,因為我認為這會導致使用已處置的對象。

class CustomWebHostService : WebHostService
{
  // From https://msdn.microsoft.com/en-us/library/windows/desktop/ms681385(v=vs.85).aspx
  private const int ErrorBadConfiguration = 1610;

  public CustomWebHostService(IWebHost host) : base(host)
  {
  }

  protected override void OnStarting(string[] args)
  {
    base.OnStarting(args);

    var customConfig = ReadAndValidateCustomConfig();
    if (customConfig == null)
    {
      ExitCode = ErrorBadConfiguration;
      Stop();
    }
  }
}
// Code from https://github.com/aspnet/Hosting/blob/2.0.1/src/Microsoft.AspNetCore.Hosting.WindowsServices/WebHostService.cs
public class WebHostService : ServiceBase
{
  protected sealed override void OnStart(string[] args)
  {
    OnStarting(args);

    _host
      .Services
      .GetRequiredService<IApplicationLifetime>()
      .ApplicationStopped
      .Register(() =>
      {
        if (!_stopRequestedByWindows)
        {
          Stop();
        }
      });

    _host.Start();

    OnStarted();
  }

  protected sealed override void OnStop()
  {
    _stopRequestedByWindows = true;
    OnStopping();
    _host?.Dispose();
    OnStopped();
  }

具體來說, CustomWebHostService.OnStarting()將調用ServiceBase.Stop() ,后者又將調用WebHostService.OnStop()來處理this._host 然后,使用this._host WebServiceHost.OnStart()的第二條語句。

令人困惑的是,由於CustomWebHostService.OnStarted()最終沒有在我的實驗中被調用,因此這種方法實際上似乎“可行”。 但是,我懷疑這是因為事先拋出了異常。 這似乎不是應該依靠的東西,因此感覺也不是一個特別強大的解決方案。

Windows服務在啟動過程中失敗的正確方法是通過延遲ServiceBase.Stop()調用(即讓Windows Service啟動,然后停止它的配置結果是錯誤的)提出了另一種方法,但這似乎通過在將來的任意時間停止Windows服務,允許Web API開始啟動,然后再將腿掃出Web API。

我認為,如果Windows服務可以告訴它不應該啟動Web API,或者啟動Web API讀取配置並關閉自身,則我不希望啟動Web API (請參閱選項3)。

同樣,仍然不清楚如何使customConfig實例可用於ASP.NET Core 2 Web Service類。

選項3:在初始化Web Service的啟動類期間

這似乎是讀取配置的最佳位置,因為配置在邏輯上屬於Web服務,並且與托管無關。

我發現IApplicationLifetime接口具有StopApplication()方法(請參閱ASP.NET Core中Hosting的 IApplicationLifetime部分)。 這似乎很理想。

public class Program
{
  public static void Main(string[] args)
  {
    if (Debugger.IsAttached)
    {
      BuildWebHost(args).Run();
    }
    else
    {
      BuildWebHost(args).RunAsCustomService();
    }
  }

  public static IWebHost BuildWebHost(string[] args) =>
      WebHost.CreateDefaultBuilder(args)
          .UseStartup<Startup>()
          .UseUrls("http://*:5000")
          .Build();
}

public class Startup
{
  public Startup(IConfiguration configuration)
  {
    Configuration = configuration;
  }

  public IConfiguration Configuration { get; }

  // This method gets called by the runtime. Use this method to add services to the container.
  public void ConfigureServices(IServiceCollection services)
  {
    services.AddMvc();
  }

  // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
  public void Configure(IApplicationBuilder app, IApplicationLifetime appLifetime, IHostingEnvironment env)
  {
    appLifetime.ApplicationStarted.Register(OnStarted);
    appLifetime.ApplicationStopping.Register(OnStopping);
    appLifetime.ApplicationStopped.Register(OnStopped);

    var customConfig = ReadAndValidateCustomConfig();
    if (customConfig != null)
    {
      // TODO: Somehow make config available to MVC controllers

      app.UseMvc();
    }
    else
    {
      appLifetime.StopApplication();
    }
  }

  private void OnStarted()
  {
    // Perform post-startup activities here
    Console.WriteLine("OnStarted");
  }

  private void OnStopping()
  {
    // Perform on-stopping activities here
    Console.WriteLine("OnStopping");
  }

  private void OnStopped()
  {
    // Perform post-stopped activities here
    Console.WriteLine("OnStopped");
  }
}

在Visual Studio中進行調試時,此方法“有效”(即,如果配置良好,則進程獨立啟動並提供Web API;如果不良,則干凈地關閉該進程),盡管日志消息的顯示順序有些奇怪使我感到懷疑:

OnStopping
OnStarted
Hosting environment: Development
Content root path: [redacted]
Now listening on: http://[::]:5000
Application started. Press Ctrl+C to shut down.
OnStopped

作為配置無效的Windows服務啟動時,該服務保持運行狀態,但是Web API始終以404(未找到)響應。
據推測,這意味着Web API已關閉,但是Windows Service尚未注意到這一點,因此也未關閉自身。

再次查看IApplication文檔,我注意到它說:

IApplicationLifetime接口允許您執行啟動后和關閉后的活動。

這表明我真的不應該在啟動過程中調用StopApplication() 但是,在應用程序啟動之后,我看不到將呼叫延遲的方法。

如果這不可能,是否有另一種方式向ASP.NET Core 2發出信號,通知Startup.Configure()函數已失敗並且應關閉應用程序?

結論

我們歡迎任何有關達到上述要求的好方法的建議,並指出我嚴重誤解了Windows Services和/或ASP.NET Core 2! :-)

在HTTP.sys Windows服務中托管的Web API服務中,我讀取了Window服務的OnStart方法中的配置。 您可以在if (File.Exists(configurationFile))添加else語句,然后記錄日志,然后拋出異常以停止啟動。

using System;
using System.IO;
using System.ServiceProcess;
using Cotg.Core2.Contract;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Newtonsoft.Json.Linq;


namespace Company.Test.Service
{
    public class WindowsService : ServiceBase
    {
        public static AppSettings AppSettings;
        public static ICoreLogger Logger;
        private IWebHost _webHost;


        protected override void OnStart(string[] Args)
        {
            // Parse configuration file.
            string contentRoot = AppDomain.CurrentDomain.BaseDirectory;
            const string environmentalVariableName = "ASPNETCORE_ENVIRONMENT";
            string environment = Environment.GetEnvironmentVariable(environmentalVariableName) ?? EnvironmentNames.Development;
            string configurationFile = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "AppSettings.json");
            if (File.Exists(configurationFile))
            {
                JObject configuration = JObject.Parse(File.ReadAllText(configurationFile));
                AppSettings = configuration.GetValue(environment).ToObject<AppSettings>();
            }
            // Create logger.
            Logger = new ConcurrentDatabaseLogger(Environment.MachineName, AppSettings.AppName, AppSettings.ProcessName, AppSettings.AppLogsDatabase);
            // Create Http.sys web host.
            Logger.Log("Creating web host using HTTP.sys.");
            _webHost = WebHost.CreateDefaultBuilder(Args)
                .UseStartup<WebApiStartup>()
                .UseContentRoot(contentRoot)
                .UseHttpSys(Options =>
                {
                    Options.Authentication.Schemes = AppSettings.AuthenticationSchema;
                    Options.Authentication.AllowAnonymous = AppSettings.AllowAnonymous;
                    Options.MaxConnections = AppSettings.MaxConnections;
                    Options.MaxRequestBodySize = AppSettings.MaxRequestBodySize;
                    // Allow external access to service by opening TCP port via an Inbound Rule in Windows Firewall.
                    // To run service using a non-administrator account, grant the user access to URL via netsh.
                    //   Use an exact URL: netsh http add urlacl url=http://servername:9092/ user=domain\user.
                    //   Do not use a wildcard URL: netsh http add urlacl url=http://+:9092/ user=domain\user.
                    Options.UrlPrefixes.Add(AppSettings.Url);
                })
                .Build();
            _webHost.Start();
            Logger.Log($"Web host started.  Listening on {AppSettings.Url}.");
        }


        protected override void OnStop()
        {
            _webHost?.StopAsync(TimeSpan.FromSeconds(60));
        }
    }
}

我不必擔心ASP.NET Core的ConfigBuilder,因為在調用ASP.NET Configure&ConfigureServices方法之前,Windows服務啟動中需要一個強類型的配置類。 我只是向ASP.NET Core的依賴項注入注冊了AppSettings類,因此任何需要它的控制器都可以使用它。

public void ConfigureServices(IServiceCollection Services)
{
    Services.AddMvc();
    // Configure dependency injection.
    Services.AddSingleton(typeof(AppSettings), WindowsService.AppSettings);
    Services.AddSingleton(typeof(ICoreLogger), WindowsService.Logger);
}

AppSettings.json具有適用於所有環境的條目:

{
  "Dev": {
    "Key1": "Value1",
    "Key2": "Value2"
  },
  "Test": {
    "Key1": "Value1",
    "Key2": "Value2"
  },
  "Prod": {
    "Key1": "Value1",
    "Key2": "Value2"
  }
}

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM