.NET Worker Service 作为 Windows 服务运行及优雅退出改进
上一篇文章我們了解了如何為 Worker Service 添加 Serilog 日志記錄,今天我接著介紹一下如何將 Worker Service 作為 Windows 服務運行。
我曾經在前面一篇文章的總結中提到過可以使用?sc.exe?實用工具將 Worker Service 安裝為 Windows 服務運行,本文中我就來具體闡述一下如何實現它。
SC 是什么?
sc.exe?是包含于 Windows SDK 的,可用于控制服務的命令行實用程序,它的命令對應于服務控制管理器(SCM)[1]?提供的函數。
服務控制管理器(SCM) 是 Windows NT 系列操作系統中的一個特殊進程,它在操作系統啟動時由 wininit 進程啟動,用于啟動和停止 Windows 進程(包括設備驅動程序和啟動程序)。SCM 的主要功能是在系統啟動時啟動所有必需的服務,它類似于類 Unix 系統上的 init 進程(或者現代 Linux 發行版上使用的較新的 systemd init 系統),用于啟動各種系統守護進程[2]。SCM 是一個遠程過程調用(RPC)服務,服務配置和服務控制程序可以借它來控制遠程計算機上的服務。
打開 Windows 命令提示符窗口,輸入并運行?sc?命令,您便可以看到?sc.exe?實用工具的幫助信息:
> sc描述:SC 是用來與服務控制管理器和服務進行通信的命令行程序。 用法:sc <server> [command] [service name] <option1> <option2>...<server> 選項的格式為 "\\ServerName"可通過鍵入以下命令獲取有關命令的更多幫助: "sc [command]"命令:query-----------查詢服務的狀態,或枚舉服務類型的狀態。queryex---------查詢服務的擴展狀態,或枚舉服務類型的狀態。start-----------啟動服務。pause-----------向服務發送 PAUSE 控制請求。interrogate-----向服務發送 INTERROGATE 控制請求。continue--------向服務發送 CONTINUE 控制請求。stop------------向服務發送 STOP 請求。config----------更改服務的配置(永久)。description-----更改服務的描述。failure---------更改失敗時服務執行的操作。failureflag-----更改服務的失敗操作標志。sidtype---------更改服務的服務 SID 類型。privs-----------更改服務的所需特權。managedaccount--更改服務以將服務帳戶密碼標記為由 LSA 管理。qc--------------查詢服務的配置信息。qdescription----查詢服務的描述。qfailure--------查詢失敗時服務執行的操作。qfailureflag----查詢服務的失敗操作標志。qsidtype--------查詢服務的服務 SID 類型。qprivs----------查詢服務的所需特權。qtriggerinfo----查詢服務的觸發器參數。qpreferrednode--查詢服務的首選 NUMA 節點。qmanagedaccount-查詢服務是否將帳戶與 LSA 管理的密碼結合使用。qprotection-----查詢服務的進程保護級別。quserservice----查詢用戶服務模板的本地實例。delete ----------(從注冊表中)刪除服務。create----------創建服務(并將其添加到注冊表中)。control---------向服務發送控制。sdshow----------顯示服務的安全描述符。sdset-----------設置服務的安全描述符。showsid---------顯示與任意名稱對應的服務 SID 字符串。triggerinfo-----配置服務的觸發器參數。preferrednode---設置服務的首選 NUMA 節點。GetDisplayName--獲取服務的 DisplayName。GetKeyName------獲取服務的 ServiceKeyName。EnumDepend------枚舉服務依賴關系。 ...您可以從幫助信息中看到?sc?實用工具支持的所有命令集及其介紹。我們在本文中要用到的命令有:
create----------創建服務(并將其添加到注冊表中)
description-----更改服務的描述。
start-----------啟動服務。
stop------------向服務發送 STOP 請求。
delete ----------(從注冊表中)刪除服務。
創建項目并發布
§下載 Worker Service 源碼
我將基于上一篇文章中的 Worker Service 源碼[3]來修改,如果您安裝有 git,可以用下面的命令獲取它:
git clone git@github.com:ITTranslate/WorkerServiceWithSerilog.git然后,使用 Visual Studio Code 打開此項目,運行一下,以確保一切正常:
dotnet build dotnet run§添加 Windows Services 依賴
為了作為 Windows 服務運行,我們需要我們的 Worker 監聽來自?ServiceBase?的啟動和停止信號,ServiceBase?是將 Windows 服務系統公開給 .NET 應用程序的 .NET 類型。為此,我們需要添加?Microsoft.Extensions.Hosting.WindowsServices?NuGet 包:
dotnet add package Microsoft.Extensions.Hosting.WindowsServices然后修改?Program.cs?中的?CreateHostBuilder?方法,添加?UseWindowsService?方法調用:
public static IHostBuilder CreateHostBuilder(string[] args) =>Host.CreateDefaultBuilder(args).UseWindowsService() // Sets the host lifetime to WindowsServiceLifetime....ConfigureServices((hostContext, services) =>{services.AddHostedService<Worker>();}).UseSerilog(); //將 Serilog 設置為日志提供程序然后,運行一下構建命令,確保一切正常:
dotnet build不出意外,您會看到?已成功生成?的提示。
§發布程序
運行?dotnet publish?命令將應用程序及其依賴項發布到文件夾(我的操作系統是 win10 x64 系統)[4]。
dotnet publish -c Release -r win-x64 -o c:\test\workerpub命令運行完成后,您會在?C:\test\workerpub?文件夾中看到可執行程序及其所有依賴項。
創建并運行服務
首先,需要特別注意的是:當我們使用?sc.exe?實用工具管理服務時,必須以管理員身份運行 Windows 命令提示符,否則會執行失敗。
§安裝服務
安裝服務我們需要用到創建服務命令 ——?sc create。
以管理員身份打開 Windows 命令提示符窗口,輸入并運行?sc create?命令,可以看到此命令的的幫助信息:
> sc create描述:在注冊表和服務數據庫中創建服務項。 用法:sc <server> create [service name] [binPath= ] <option1> <option2>...選項: 注意: 選項名稱包括等號。等號和值之間需要一個空格。type= <own|share|interact|kernel|filesys|rec|userown|usershare>(默認 = own)start= <boot|system|auto|demand|disabled|delayed-auto>(默認 = demand)error= <normal|severe|critical|ignore>(默認 = normal)binPath= <.exe 文件的 BinaryPathName>group= <LoadOrderGroup>tag= <yes|no>depend= <依存關系(以 / (斜杠)分隔)>obj= <AccountName|ObjectName>(默認= LocalSystem)DisplayName= <顯示名稱>password= <密碼>命令?sc create?的參數說明[5]:
server:指定服務所在的遠程服務器的名稱。名稱必須使用通用命名約定(UNC)格式 (例如,\myserver) 。若要在本地運行 SC.exe,請不要使用此參數。
service name:指定?getkeyname?操作返回的服務名稱。
binPath=:指定服務二進制文件的路徑。binPath= 沒有默認值,必須提供此字符串。
displayname= "顯示名稱":指定一個友好名稱,用于標識用戶界面程序中的服務。
start= {boot|system|auto|demand|disabled|delayed-auto}:指定服務的啟動類型。選項包括:
boot - 指定由啟動加載程序加載的設備驅動程序。
system - 指定在內核初始化過程中啟動的設備驅動程序。
auto - 指定一項服務,該服務在計算機每次重新啟動時自動啟動并運行(即使沒有人登錄到計算機)。
demand - 指定必須手動啟動的服務。如果未指定 start= ,則此值為默認值。
disabled - 指定無法啟動的服務。若要啟動已禁用的服務,請將啟動類型更改為其他某個值。
delayed-auto - 指定一項服務,該服務將在啟動其他自動服務之后的短時間自動啟動。
注意事項:
1、每個命令行選項 (參數) 必須包含等號作為選項名稱的一部分。
2、選項與其值之間必須有一個空格(例如,type= own),如果遺漏了空格,操作將失敗。
了解了?sc create?命令的用法,不難得出此處我們所需要的命令如下:
sc create MyService binPath= "C:\test\workerpub\MyService.exe" start= auto displayname= "技術譯站的測試服務"運行以上命令,輸出以下結果:
[SC] CreateService 成功運行?services.msc?命令打開本地服務列表,可以看到我們的服務已經安裝好了,服務名稱顯示為?技術譯站的測試服務。它沒有描述,處于已停止狀態。
§設置服務的描述
輸入并運行?sc description?命令,可以看到此命令的的幫助信息:
> sc description 描述:設置服務的描述字符串。 用法:sc <server> description [service name] [description]運行以下命令給該服務添加描述信息:
sc description MyService "這是一個由 Worker Service 實現的測試服務。"輸出結果:
[SC] ChangeServiceConfig2 成功運行成功以后,按?F5?刷新服務列表,您將看到服務描述已經更新了。
§啟動服務
輸入并運行?sc start?命令,可以看到此命令的的幫助信息:
> sc start描述:啟動服務運行。 用法:sc <server> start [service name] <arg1> <arg2> ...輸入以下命令啟動服務:
sc start MyService輸出結果:
[SC] StartService 失敗 1053:服務沒有及時響應啟動或控制請求。啟動失敗了,為什么呢?查看一下 Windows 事件查看器 --> 應用程序,顯示的錯誤原因大致如下:
The process was terminated due to an unhandled exception. Exception Info: System.IO.FileNotFoundException: The configuration file 'appsettings.json' was not found and is not optional. The physical path is 'C:\WINDOWS\system32\appsettings.json'.回頭看一下?Program.cs?文件,在?Main?方法中我們為配置設置的基路徑是?Directory.GetCurrentDirectory()。但是作為 Windows Service 運行時,默認的當前工作目錄是?C:\WINDOWS\system32,所以導致了這樣的錯誤。為了解決這一問題,我們需要在設置配置的基路徑前添加一行?Directory.SetCurrentDirectory(AppContext.BaseDirectory),代碼如下:
// 作為 Windows Service 運行時,默認的當前工作目錄是 C:\WINDOWS\system32,會導致找不到配置文件, // 所以需要添加下面一行,指定當前工作目錄為應用程序所在的實際目錄。 Directory.SetCurrentDirectory(AppContext.BaseDirectory);var configuration = new ConfigurationBuilder().SetBasePath(Directory.GetCurrentDirectory()).AddJsonFile("appsettings.json").AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT") ?? "Production"}.json", true).Build();作為 Windows Service 運行時,默認情況下,Directory.GetCurrentDirectory() 為?C:\WINDOWS\system32,
AppDomain.CurrentDomain.BaseDirectory 和 AppContext.BaseDirectory 為應用程序所在的實際目錄。
因為在有的依賴程序包中有用到 Directory.GetCurrentDirectory() 獲取來程序所在目錄,所以這里必須使用 Directory.SetCurrentDirectory 設置當前工作目錄。
再次啟動服務:
> sc start MyServiceSERVICE_NAME: MyServiceTYPE : 10 WIN32_OWN_PROCESSSTATE : 2 START_PENDING(NOT_STOPPABLE, NOT_PAUSABLE, IGNORES_SHUTDOWN)WIN32_EXIT_CODE : 0 (0x0)SERVICE_EXIT_CODE : 0 (0x0)CHECKPOINT : 0x0WAIT_HINT : 0x7d0PID : 21736FLAGS :這次服務啟動成功了。
§停止服務
運行以下命令,停止?MyService?服務。
sc stop MyService輸出結果:
SERVICE_NAME: MyServiceTYPE : 10 WIN32_OWN_PROCESSSTATE : 3 STOP_PENDING(STOPPABLE, NOT_PAUSABLE, ACCEPTS_SHUTDOWN)WIN32_EXIT_CODE : 0 (0x0)SERVICE_EXIT_CODE : 0 (0x0)CHECKPOINT : 0x0WAIT_HINT : 0x0§刪除服務
運行以下命令,(從注冊表中)刪除?MyService?服務。
sc delete MyService輸出結果:
[SC] DeleteService 成功至此,我們使用?sc?實用工具演示了服務的創建、更改描述、啟動、停止和刪除。當服務創建完成以后,您也可以使用 Windows 服務管理器來維護服務的啟動、停止等。
Windows Service 優雅退出
§問題
我查看了一下?C:\test\workerpub\Logs?目錄下的日志信息,發現當停止服務的時候,它并沒有像我將 Worker Service 作為控制臺應用運行時那樣優雅退出(等待關閉前必須完成的任務正常結束后再退出)。也就是說,我在.NET Worker Service 如何優雅退出[6]中使用的方法,在將 Worker Service 作為 Windows 服務運行時失效了。
這是什么原因呢,該如何解決呢?
§查找原因
我們來看一下?UseWindowsService?方法的源代碼:
其中有這樣一行:
// https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/src/WindowsServiceLifetimeHostBuilderExtensions.csservices.AddSingleton<IHostLifetime, WindowsServiceLifetime>();也就是說,當 Worker Service 作為 Windows Service 運行時,使用的宿主(Host)生命周期控制類不再是作為控制臺應用運行時的?ConsoleLifetime,而是?WindowsServiceLifetime,它派生自?ServiceBase。
讓我們來看一下?WindowsServiceLifetime?的源代碼:
您會發現?WindowsServiceLifetime?類的?OnStop?和?OnShutdown?方法中調用了?ApplicationLifetime.StopApplication();而它的基類?ServiceBase?中,當服務停止時調用了?OnStop?和?OnShutdown?方法。也就是說,在 Windows 服務停止的時候已經調用了?ApplicationLifetime.StopApplication()。這就是我們在?Worker?中手動調用?StopApplication?失效的原因。
問題的原因找到了,該怎么解決它呢?
§解決方法
功夫不負有心人,在認真查閱了?dotnet runtime[7]?中?BackgroundService?、WindowsServiceLifetime?和?ApplicationLifetime?類的源代碼后,終于找到了解決方法。既然?WindowsServiceLifetime?中調用了?StopApplication,那我就換別的方法唄。
注意到?ApplicationLifetime?的屬性?ApplicationStopping(類型為?CancellationToken),它的注釋是:
Triggered when the application host is performing a graceful shutdown. Request may still be in flight. Shutdown will block until this event completes.
所以,我們可以向它注冊一個取消時調用的的委托操作。修改一下?Worker?類中的?StartAsync?方法,添加以下代碼:
// 注冊應用停止前需要完成的操作 _hostApplicationLifetime.ApplicationStopping.Register(() => {GetOffWork(); });向 ApplicationStopping 注冊的委托,在?StopAsync?之前運行。
修改后?Worker?類的完整代碼如下:
public class Worker : BackgroundService {/// <summary>/// 狀態:0-默認狀態,1-正在完成關閉前的必要工作,2-正在執行 StopAsync/// </summary>private volatile int _status = 0; //狀態private readonly IHostApplicationLifetime _hostApplicationLifetime;private readonly ILogger<Worker> _logger;public Worker(IHostApplicationLifetime hostApplicationLifetime, ILogger<Worker> logger){_hostApplicationLifetime = hostApplicationLifetime;_logger = logger;}public override Task StartAsync(CancellationToken cancellationToken){// 注冊應用停止前需要完成的操作_hostApplicationLifetime.ApplicationStopping.Register(() =>{GetOffWork();});_logger.LogInformation("上班了,又是精神抖擻的一天,output from StartAsync");return base.StartAsync(cancellationToken);}protected override async Task ExecuteAsync(CancellationToken stoppingToken){try{// 這里實現實際的業務邏輯while (!stoppingToken.IsCancellationRequested){try{_logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);await SomeMethodThatDoesTheWork(stoppingToken);}catch (Exception ex){_logger.LogError(ex, "Global exception occurred. Will resume in a moment.");}await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);}}finally{_logger.LogWarning("My worker service shut down.");}}private async Task SomeMethodThatDoesTheWork(CancellationToken cancellationToken){string msg = _status switch{1 => "正在完成關閉前的必要工作……",2 => "假裝還在埋頭苦干ing…… 其實我去洗杯子了",_ => "我愛工作,埋頭苦干ing……"};_logger.LogInformation(msg);await Task.CompletedTask;}/// <summary>/// 關閉前需要完成的工作/// </summary>private void GetOffWork(){_status = 1;_logger.LogInformation("太好了,下班時間到,output from ApplicationStopping.Register Action at: {time}", DateTimeOffset.Now); _logger.LogDebug("開始處理關閉前必須完成的工作 at: {time}", DateTimeOffset.Now);_logger.LogInformation("糟糕,有一個緊急 bug 需要下班前完成!!!");_logger.LogInformation("啊啊啊,我愛加班,我要再干 20 秒,Wait 1 ");Task.Delay(TimeSpan.FromSeconds(20)).Wait();_logger.LogInformation("啊啊啊啊啊啊,我愛加班,我要再干 1 分鐘,Wait 2 ");Task.Delay(TimeSpan.FromMinutes(1)).Wait();_logger.LogInformation("啊哈哈哈哈哈,終于好了,可以下班了!");_logger.LogDebug("關閉前必須完成的工作處理完成 at: {time}", DateTimeOffset.Now);}public override Task StopAsync(CancellationToken cancellationToken){_status = 2;_logger.LogInformation("準備下班了,output from StopAsync at: {time}", DateTimeOffset.Now);_logger.LogInformation("去洗洗茶杯先……", DateTimeOffset.Now);Task.Delay(30_000).Wait();_logger.LogInformation("茶杯洗好了。", DateTimeOffset.Now);_logger.LogInformation("下班嘍 ^_^", DateTimeOffset.Now);return base.StopAsync(cancellationToken);} }代碼修改完成以后,停止服務,重新發布程序。
dotnet publish -c Release -r win-x64 -o c:\test\workerpub再次啟動服務然后關閉服務,您會發現,我們編寫的 Windows Service 已經可以優雅退出了。
這種方法,不僅作為 Windows 服務運行時可以優雅退出,而且作為控制臺應用運行時也一樣適用,它比我在.NET Worker Service 如何優雅退出中介紹的方法更加完美。
總結
在本文中,我通過一個實例詳細介紹了如何將 .NET Worker Service 作為 Windows 服務運行,并說明了如何使用?sc.exe?實用工具安裝和管理服務。還改進了 Worker Service 優雅退出的方法,使它不僅適用于控制臺應用而且適用于 Windows 服務。
當我們向?HostBuilder?添加了?.UseWindowsService()?方法調用后,編譯出的程序,既可以作為控制臺應用運行,也可以作為 Windows 服務運行。
您可以從 GitHub?下載本文中的源碼[8]。
相關閱讀:
.NET Worker Service 入門介紹
.NET Worker Service 如何優雅退出
.NET Worker Service 添加 Serilog 日志記錄
相關鏈接:
https://docs.microsoft.com/zh-cn/windows/win32/services/service-control-manager???
https://www.techopedia.com/definition/25522/service-control-manager-scm???
https://github.com/ITTranslate/WorkerServiceWithSerilog???
https://docs.microsoft.com/zh-cn/dotnet/core/tools/dotnet-publish???
https://docs.microsoft.com/zh-cn/windows-server/administration/windows-commands/sc-create???
https://mp.weixin.qq.com/s/voxAxh9rQQogE3_Yc1-eCQ???
https://github.com/dotnet/runtime?dotnet runtime???
https://github.com/ITTranslate/WorkerServiceAsWindowsService?源碼下載???
作者 :技術譯民
出品 :技術譯站(https://ITTranslator.cn/)
END
創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎總結
以上是生活随笔為你收集整理的.NET Worker Service 作为 Windows 服务运行及优雅退出改进的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 你有做 Code Review 吗?
- 下一篇: C# 外接(网口)双摄像头视频获取