.NET Worker Service 作为 Windows 服务运行及优雅退出改进
上一篇文章我們了解了如何為 Worker Service 添加 Serilog 日志記錄,今天我接著介紹一下如何將 Worker Service 作為 Windows 服務(wù)運(yùn)行。
我曾經(jīng)在前面一篇文章的總結(jié)中提到過可以使用?sc.exe?實(shí)用工具將 Worker Service 安裝為 Windows 服務(wù)運(yùn)行,本文中我就來具體闡述一下如何實(shí)現(xiàn)它。
SC 是什么?
sc.exe?是包含于 Windows SDK 的,可用于控制服務(wù)的命令行實(shí)用程序,它的命令對應(yīng)于服務(wù)控制管理器(SCM)[1]?提供的函數(shù)。
服務(wù)控制管理器(SCM) 是 Windows NT 系列操作系統(tǒng)中的一個(gè)特殊進(jìn)程,它在操作系統(tǒng)啟動(dòng)時(shí)由 wininit 進(jìn)程啟動(dòng),用于啟動(dòng)和停止 Windows 進(jìn)程(包括設(shè)備驅(qū)動(dòng)程序和啟動(dòng)程序)。SCM 的主要功能是在系統(tǒng)啟動(dòng)時(shí)啟動(dòng)所有必需的服務(wù),它類似于類 Unix 系統(tǒng)上的 init 進(jìn)程(或者現(xiàn)代 Linux 發(fā)行版上使用的較新的 systemd init 系統(tǒng)),用于啟動(dòng)各種系統(tǒng)守護(hù)進(jìn)程[2]。SCM 是一個(gè)遠(yuǎn)程過程調(diào)用(RPC)服務(wù),服務(wù)配置和服務(wù)控制程序可以借它來控制遠(yuǎn)程計(jì)算機(jī)上的服務(wù)。
打開 Windows 命令提示符窗口,輸入并運(yùn)行?sc?命令,您便可以看到?sc.exe?實(shí)用工具的幫助信息:
> sc描述:SC 是用來與服務(wù)控制管理器和服務(wù)進(jìn)行通信的命令行程序。 用法:sc <server> [command] [service name] <option1> <option2>...<server> 選項(xiàng)的格式為 "\\ServerName"可通過鍵入以下命令獲取有關(guān)命令的更多幫助: "sc [command]"命令:query-----------查詢服務(wù)的狀態(tài),或枚舉服務(wù)類型的狀態(tài)。queryex---------查詢服務(wù)的擴(kuò)展?fàn)顟B(tài),或枚舉服務(wù)類型的狀態(tài)。start-----------啟動(dòng)服務(wù)。pause-----------向服務(wù)發(fā)送 PAUSE 控制請求。interrogate-----向服務(wù)發(fā)送 INTERROGATE 控制請求。continue--------向服務(wù)發(fā)送 CONTINUE 控制請求。stop------------向服務(wù)發(fā)送 STOP 請求。config----------更改服務(wù)的配置(永久)。description-----更改服務(wù)的描述。failure---------更改失敗時(shí)服務(wù)執(zhí)行的操作。failureflag-----更改服務(wù)的失敗操作標(biāo)志。sidtype---------更改服務(wù)的服務(wù) SID 類型。privs-----------更改服務(wù)的所需特權(quán)。managedaccount--更改服務(wù)以將服務(wù)帳戶密碼標(biāo)記為由 LSA 管理。qc--------------查詢服務(wù)的配置信息。qdescription----查詢服務(wù)的描述。qfailure--------查詢失敗時(shí)服務(wù)執(zhí)行的操作。qfailureflag----查詢服務(wù)的失敗操作標(biāo)志。qsidtype--------查詢服務(wù)的服務(wù) SID 類型。qprivs----------查詢服務(wù)的所需特權(quán)。qtriggerinfo----查詢服務(wù)的觸發(fā)器參數(shù)。qpreferrednode--查詢服務(wù)的首選 NUMA 節(jié)點(diǎn)。qmanagedaccount-查詢服務(wù)是否將帳戶與 LSA 管理的密碼結(jié)合使用。qprotection-----查詢服務(wù)的進(jìn)程保護(hù)級別。quserservice----查詢用戶服務(wù)模板的本地實(shí)例。delete ----------(從注冊表中)刪除服務(wù)。create----------創(chuàng)建服務(wù)(并將其添加到注冊表中)。control---------向服務(wù)發(fā)送控制。sdshow----------顯示服務(wù)的安全描述符。sdset-----------設(shè)置服務(wù)的安全描述符。showsid---------顯示與任意名稱對應(yīng)的服務(wù) SID 字符串。triggerinfo-----配置服務(wù)的觸發(fā)器參數(shù)。preferrednode---設(shè)置服務(wù)的首選 NUMA 節(jié)點(diǎn)。GetDisplayName--獲取服務(wù)的 DisplayName。GetKeyName------獲取服務(wù)的 ServiceKeyName。EnumDepend------枚舉服務(wù)依賴關(guān)系。 ...您可以從幫助信息中看到?sc?實(shí)用工具支持的所有命令集及其介紹。我們在本文中要用到的命令有:
create----------創(chuàng)建服務(wù)(并將其添加到注冊表中)
description-----更改服務(wù)的描述。
start-----------啟動(dòng)服務(wù)。
stop------------向服務(wù)發(fā)送 STOP 請求。
delete ----------(從注冊表中)刪除服務(wù)。
創(chuàng)建項(xiàng)目并發(fā)布
§下載 Worker Service 源碼
我將基于上一篇文章中的 Worker Service 源碼[3]來修改,如果您安裝有 git,可以用下面的命令獲取它:
git clone git@github.com:ITTranslate/WorkerServiceWithSerilog.git然后,使用 Visual Studio Code 打開此項(xiàng)目,運(yùn)行一下,以確保一切正常:
dotnet build dotnet run§添加 Windows Services 依賴
為了作為 Windows 服務(wù)運(yùn)行,我們需要我們的 Worker 監(jiān)聽來自?ServiceBase?的啟動(dòng)和停止信號,ServiceBase?是將 Windows 服務(wù)系統(tǒng)公開給 .NET 應(yīng)用程序的 .NET 類型。為此,我們需要添加?Microsoft.Extensions.Hosting.WindowsServices?NuGet 包:
dotnet add package Microsoft.Extensions.Hosting.WindowsServices然后修改?Program.cs?中的?CreateHostBuilder?方法,添加?UseWindowsService?方法調(diào)用:
public static IHostBuilder CreateHostBuilder(string[] args) =>Host.CreateDefaultBuilder(args).UseWindowsService() // Sets the host lifetime to WindowsServiceLifetime....ConfigureServices((hostContext, services) =>{services.AddHostedService<Worker>();}).UseSerilog(); //將 Serilog 設(shè)置為日志提供程序然后,運(yùn)行一下構(gòu)建命令,確保一切正常:
dotnet build不出意外,您會(huì)看到?已成功生成?的提示。
§發(fā)布程序
運(yùn)行?dotnet publish?命令將應(yīng)用程序及其依賴項(xiàng)發(fā)布到文件夾(我的操作系統(tǒng)是 win10 x64 系統(tǒng))[4]。
dotnet publish -c Release -r win-x64 -o c:\test\workerpub命令運(yùn)行完成后,您會(huì)在?C:\test\workerpub?文件夾中看到可執(zhí)行程序及其所有依賴項(xiàng)。
創(chuàng)建并運(yùn)行服務(wù)
首先,需要特別注意的是:當(dāng)我們使用?sc.exe?實(shí)用工具管理服務(wù)時(shí),必須以管理員身份運(yùn)行 Windows 命令提示符,否則會(huì)執(zhí)行失敗。
§安裝服務(wù)
安裝服務(wù)我們需要用到創(chuàng)建服務(wù)命令 ——?sc create。
以管理員身份打開 Windows 命令提示符窗口,輸入并運(yùn)行?sc create?命令,可以看到此命令的的幫助信息:
> sc create描述:在注冊表和服務(wù)數(shù)據(jù)庫中創(chuàng)建服務(wù)項(xiàng)。 用法:sc <server> create [service name] [binPath= ] <option1> <option2>...選項(xiàng): 注意: 選項(xiàng)名稱包括等號。等號和值之間需要一個(gè)空格。type= <own|share|interact|kernel|filesys|rec|userown|usershare>(默認(rèn) = own)start= <boot|system|auto|demand|disabled|delayed-auto>(默認(rèn) = demand)error= <normal|severe|critical|ignore>(默認(rèn) = normal)binPath= <.exe 文件的 BinaryPathName>group= <LoadOrderGroup>tag= <yes|no>depend= <依存關(guān)系(以 / (斜杠)分隔)>obj= <AccountName|ObjectName>(默認(rèn)= LocalSystem)DisplayName= <顯示名稱>password= <密碼>命令?sc create?的參數(shù)說明[5]:
server:指定服務(wù)所在的遠(yuǎn)程服務(wù)器的名稱。名稱必須使用通用命名約定(UNC)格式 (例如,\myserver) 。若要在本地運(yùn)行 SC.exe,請不要使用此參數(shù)。
service name:指定?getkeyname?操作返回的服務(wù)名稱。
binPath=:指定服務(wù)二進(jìn)制文件的路徑。binPath= 沒有默認(rèn)值,必須提供此字符串。
displayname= "顯示名稱":指定一個(gè)友好名稱,用于標(biāo)識用戶界面程序中的服務(wù)。
start= {boot|system|auto|demand|disabled|delayed-auto}:指定服務(wù)的啟動(dòng)類型。選項(xiàng)包括:
boot - 指定由啟動(dòng)加載程序加載的設(shè)備驅(qū)動(dòng)程序。
system - 指定在內(nèi)核初始化過程中啟動(dòng)的設(shè)備驅(qū)動(dòng)程序。
auto - 指定一項(xiàng)服務(wù),該服務(wù)在計(jì)算機(jī)每次重新啟動(dòng)時(shí)自動(dòng)啟動(dòng)并運(yùn)行(即使沒有人登錄到計(jì)算機(jī))。
demand - 指定必須手動(dòng)啟動(dòng)的服務(wù)。如果未指定 start= ,則此值為默認(rèn)值。
disabled - 指定無法啟動(dòng)的服務(wù)。若要啟動(dòng)已禁用的服務(wù),請將啟動(dòng)類型更改為其他某個(gè)值。
delayed-auto - 指定一項(xiàng)服務(wù),該服務(wù)將在啟動(dòng)其他自動(dòng)服務(wù)之后的短時(shí)間自動(dòng)啟動(dòng)。
注意事項(xiàng):
1、每個(gè)命令行選項(xiàng) (參數(shù)) 必須包含等號作為選項(xiàng)名稱的一部分。
2、選項(xiàng)與其值之間必須有一個(gè)空格(例如,type= own),如果遺漏了空格,操作將失敗。
了解了?sc create?命令的用法,不難得出此處我們所需要的命令如下:
sc create MyService binPath= "C:\test\workerpub\MyService.exe" start= auto displayname= "技術(shù)譯站的測試服務(wù)"運(yùn)行以上命令,輸出以下結(jié)果:
[SC] CreateService 成功運(yùn)行?services.msc?命令打開本地服務(wù)列表,可以看到我們的服務(wù)已經(jīng)安裝好了,服務(wù)名稱顯示為?技術(shù)譯站的測試服務(wù)。它沒有描述,處于已停止?fàn)顟B(tài)。
§設(shè)置服務(wù)的描述
輸入并運(yùn)行?sc description?命令,可以看到此命令的的幫助信息:
> sc description 描述:設(shè)置服務(wù)的描述字符串。 用法:sc <server> description [service name] [description]運(yùn)行以下命令給該服務(wù)添加描述信息:
sc description MyService "這是一個(gè)由 Worker Service 實(shí)現(xiàn)的測試服務(wù)。"輸出結(jié)果:
[SC] ChangeServiceConfig2 成功運(yùn)行成功以后,按?F5?刷新服務(wù)列表,您將看到服務(wù)描述已經(jīng)更新了。
§啟動(dòng)服務(wù)
輸入并運(yùn)行?sc start?命令,可以看到此命令的的幫助信息:
> sc start描述:啟動(dòng)服務(wù)運(yùn)行。 用法:sc <server> start [service name] <arg1> <arg2> ...輸入以下命令啟動(dòng)服務(wù):
sc start MyService輸出結(jié)果:
[SC] StartService 失敗 1053:服務(wù)沒有及時(shí)響應(yīng)啟動(dòng)或控制請求。啟動(dòng)失敗了,為什么呢?查看一下 Windows 事件查看器 --> 應(yīng)用程序,顯示的錯(cuò)誤原因大致如下:
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?方法中我們?yōu)榕渲迷O(shè)置的基路徑是?Directory.GetCurrentDirectory()。但是作為 Windows Service 運(yùn)行時(shí),默認(rèn)的當(dāng)前工作目錄是?C:\WINDOWS\system32,所以導(dǎo)致了這樣的錯(cuò)誤。為了解決這一問題,我們需要在設(shè)置配置的基路徑前添加一行?Directory.SetCurrentDirectory(AppContext.BaseDirectory),代碼如下:
// 作為 Windows Service 運(yùn)行時(shí),默認(rèn)的當(dāng)前工作目錄是 C:\WINDOWS\system32,會(huì)導(dǎo)致找不到配置文件, // 所以需要添加下面一行,指定當(dāng)前工作目錄為應(yīng)用程序所在的實(shí)際目錄。 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 運(yùn)行時(shí),默認(rèn)情況下,Directory.GetCurrentDirectory() 為?C:\WINDOWS\system32,
AppDomain.CurrentDomain.BaseDirectory 和 AppContext.BaseDirectory 為應(yīng)用程序所在的實(shí)際目錄。
因?yàn)樵谟械囊蕾嚦绦虬杏杏玫?Directory.GetCurrentDirectory() 獲取來程序所在目錄,所以這里必須使用 Directory.SetCurrentDirectory 設(shè)置當(dāng)前工作目錄。
再次啟動(dòng)服務(wù):
> 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 :這次服務(wù)啟動(dòng)成功了。
§停止服務(wù)
運(yùn)行以下命令,停止?MyService?服務(wù)。
sc stop MyService輸出結(jié)果:
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§刪除服務(wù)
運(yùn)行以下命令,(從注冊表中)刪除?MyService?服務(wù)。
sc delete MyService輸出結(jié)果:
[SC] DeleteService 成功至此,我們使用?sc?實(shí)用工具演示了服務(wù)的創(chuàng)建、更改描述、啟動(dòng)、停止和刪除。當(dāng)服務(wù)創(chuàng)建完成以后,您也可以使用 Windows 服務(wù)管理器來維護(hù)服務(wù)的啟動(dòng)、停止等。
Windows Service 優(yōu)雅退出
§問題
我查看了一下?C:\test\workerpub\Logs?目錄下的日志信息,發(fā)現(xiàn)當(dāng)停止服務(wù)的時(shí)候,它并沒有像我將 Worker Service 作為控制臺應(yīng)用運(yùn)行時(shí)那樣優(yōu)雅退出(等待關(guān)閉前必須完成的任務(wù)正常結(jié)束后再退出)。也就是說,我在.NET Worker Service 如何優(yōu)雅退出[6]中使用的方法,在將 Worker Service 作為 Windows 服務(wù)運(yùn)行時(shí)失效了。
這是什么原因呢,該如何解決呢?
§查找原因
我們來看一下?UseWindowsService?方法的源代碼:
其中有這樣一行:
// https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.Hosting.WindowsServices/src/WindowsServiceLifetimeHostBuilderExtensions.csservices.AddSingleton<IHostLifetime, WindowsServiceLifetime>();也就是說,當(dāng) Worker Service 作為 Windows Service 運(yùn)行時(shí),使用的宿主(Host)生命周期控制類不再是作為控制臺應(yīng)用運(yùn)行時(shí)的?ConsoleLifetime,而是?WindowsServiceLifetime,它派生自?ServiceBase。
讓我們來看一下?WindowsServiceLifetime?的源代碼:
您會(huì)發(fā)現(xiàn)?WindowsServiceLifetime?類的?OnStop?和?OnShutdown?方法中調(diào)用了?ApplicationLifetime.StopApplication();而它的基類?ServiceBase?中,當(dāng)服務(wù)停止時(shí)調(diào)用了?OnStop?和?OnShutdown?方法。也就是說,在 Windows 服務(wù)停止的時(shí)候已經(jīng)調(diào)用了?ApplicationLifetime.StopApplication()。這就是我們在?Worker?中手動(dòng)調(diào)用?StopApplication?失效的原因。
問題的原因找到了,該怎么解決它呢?
§解決方法
功夫不負(fù)有心人,在認(rèn)真查閱了?dotnet runtime[7]?中?BackgroundService?、WindowsServiceLifetime?和?ApplicationLifetime?類的源代碼后,終于找到了解決方法。既然?WindowsServiceLifetime?中調(diào)用了?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.
所以,我們可以向它注冊一個(gè)取消時(shí)調(diào)用的的委托操作。修改一下?Worker?類中的?StartAsync?方法,添加以下代碼:
// 注冊應(yīng)用停止前需要完成的操作 _hostApplicationLifetime.ApplicationStopping.Register(() => {GetOffWork(); });向 ApplicationStopping 注冊的委托,在?StopAsync?之前運(yùn)行。
修改后?Worker?類的完整代碼如下:
public class Worker : BackgroundService {/// <summary>/// 狀態(tài):0-默認(rèn)狀態(tài),1-正在完成關(guān)閉前的必要工作,2-正在執(zhí)行 StopAsync/// </summary>private volatile int _status = 0; //狀態(tài)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){// 注冊應(yīng)用停止前需要完成的操作_hostApplicationLifetime.ApplicationStopping.Register(() =>{GetOffWork();});_logger.LogInformation("上班了,又是精神抖擻的一天,output from StartAsync");return base.StartAsync(cancellationToken);}protected override async Task ExecuteAsync(CancellationToken stoppingToken){try{// 這里實(shí)現(xiàn)實(shí)際的業(yè)務(wù)邏輯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 => "正在完成關(guān)閉前的必要工作……",2 => "假裝還在埋頭苦干ing…… 其實(shí)我去洗杯子了",_ => "我愛工作,埋頭苦干ing……"};_logger.LogInformation(msg);await Task.CompletedTask;}/// <summary>/// 關(guān)閉前需要完成的工作/// </summary>private void GetOffWork(){_status = 1;_logger.LogInformation("太好了,下班時(shí)間到,output from ApplicationStopping.Register Action at: {time}", DateTimeOffset.Now); _logger.LogDebug("開始處理關(guān)閉前必須完成的工作 at: {time}", DateTimeOffset.Now);_logger.LogInformation("糟糕,有一個(gè)緊急 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("關(guān)閉前必須完成的工作處理完成 at: {time}", DateTimeOffset.Now);}public override Task StopAsync(CancellationToken cancellationToken){_status = 2;_logger.LogInformation("準(zhǔn)備下班了,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);} }代碼修改完成以后,停止服務(wù),重新發(fā)布程序。
dotnet publish -c Release -r win-x64 -o c:\test\workerpub再次啟動(dòng)服務(wù)然后關(guān)閉服務(wù),您會(huì)發(fā)現(xiàn),我們編寫的 Windows Service 已經(jīng)可以優(yōu)雅退出了。
這種方法,不僅作為 Windows 服務(wù)運(yùn)行時(shí)可以優(yōu)雅退出,而且作為控制臺應(yīng)用運(yùn)行時(shí)也一樣適用,它比我在.NET Worker Service 如何優(yōu)雅退出中介紹的方法更加完美。
總結(jié)
在本文中,我通過一個(gè)實(shí)例詳細(xì)介紹了如何將 .NET Worker Service 作為 Windows 服務(wù)運(yùn)行,并說明了如何使用?sc.exe?實(shí)用工具安裝和管理服務(wù)。還改進(jìn)了 Worker Service 優(yōu)雅退出的方法,使它不僅適用于控制臺應(yīng)用而且適用于 Windows 服務(wù)。
當(dāng)我們向?HostBuilder?添加了?.UseWindowsService()?方法調(diào)用后,編譯出的程序,既可以作為控制臺應(yīng)用運(yùn)行,也可以作為 Windows 服務(wù)運(yùn)行。
您可以從 GitHub?下載本文中的源碼[8]。
相關(guān)閱讀:
.NET Worker Service 入門介紹
.NET Worker Service 如何優(yōu)雅退出
.NET Worker Service 添加 Serilog 日志記錄
相關(guān)鏈接:
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?源碼下載???
作者 :技術(shù)譯民
出品 :技術(shù)譯站(https://ITTranslator.cn/)
END
創(chuàng)作挑戰(zhàn)賽新人創(chuàng)作獎(jiǎng)勵(lì)來咯,堅(jiān)持創(chuàng)作打卡瓜分現(xiàn)金大獎(jiǎng)總結(jié)
以上是生活随笔為你收集整理的.NET Worker Service 作为 Windows 服务运行及优雅退出改进的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 你有做 Code Review 吗?
- 下一篇: 鸿蒙操作系统如何打通 Windows 操