只有累積,沒有奇蹟

2021年9月29日 星期三

[NETCore] ASP.NET Core 建立排程服務 - 使用 Generic Host 搭配 Quartz.Net - Part 3

前言
這是 ASP.NET Core 建立排程服務 - 使用 Generic Host 搭配 Quartz.Net 系列文第三篇,這系列文的目標是使用 ASP.NET Core Generic Host 搭配排程套件 Quartz.Net,程式註冊為 Windows Service 服務執行,
第一篇介紹了 Gereric Host 泛型主機的基本設定,第二篇介紹 ASP.NET Core 與 Quartz.Net 的整合,接下來這一篇是要將開發好的應用程式註冊為 Windows Service,若有問題或是錯誤的地方歡迎各位高手給予指導
GenericHostLab SampleCode 傳送門 : http://bit.ly/2Y1mqYN
ServiceBase
我們的目標是將開發完的應用程式註冊使用 Windows Service 來執行,因此在 Service 執行的動作像是啟動、停止、關閉等動作都會希望知道並透過 log 記錄下來,才可以更清楚的瞭解其運行方式與錯誤時偵錯,在 ASP.NET 中可以透過 system.serviceprocess 來取得 Service 資訊,透過指令到 nuget 下載此套件
Install-Package System.ServiceProcess.ServiceController -Version 4.5.0
接著新增一個類別 ServiceBaseLifetime 繼承 system.ServiceProcess 命名空間中的 ServiceBase 類別與實作 IHostLifetime 介面,在覆寫 serviceBase 方法像是 OnStart、OnStop 以及 run 等,並在希望關注的方法中加上 log 方便蒐集更多 Service 執行時的 log 資訊,代碼如下
namespace GenericHostLab.Service
{
    public class ServiceBaseLifetime : ServiceBase, IHostLifetime
    {
        private readonly TaskCompletionSource<object> delayStart = new TaskCompletionSource<object>();

        public ServiceBaseLifetime(IApplicationLifetime applicationLifetime)
        {
            ApplicationLifetime = applicationLifetime ?? throw new ArgumentNullException(nameof(applicationLifetime));
        }

        private IApplicationLifetime ApplicationLifetime { get; }

        public Task WaitForStartAsync(CancellationToken cancellationToken)
        {
            cancellationToken.Register(() => delayStart.TrySetCanceled());
            ApplicationLifetime.ApplicationStopping.Register(Stop);
            ApplicationLifetime.ApplicationStopped.Register(Stop);
            new Thread(Run).Start();
            return delayStart.Task;
        }

        public Task StopAsync(CancellationToken cancellationToken)
        {
            Stop();
            return Task.CompletedTask;
        }

        private void Run()
        {
            try
            {
                Run(this);
                delayStart.TrySetException(new InvalidOperationException("Stopped without starting"));
            }
            catch (Exception ex)
            {
                delayStart.TrySetException(ex);
            }
        }

        protected override void OnStart(string[] args)
        {
            delayStart.TrySetResult(null);
            base.OnStart(args);
        }
        protected override void OnStop()
        {
            ApplicationLifetime.StopApplication();
            base.OnStop();
        }

        protected override void OnShutdown()
        {
            base.OnShutdown();
        }
    }
}
新增 ServiceBaseLifetime 類別後,下一步可以針對 IHostBuilder 加入擴充方法,方便在 Main 中直接使用,在 Service 中新增 ServiceBaseLifetimeHostExtensions 類別,代碼如下
namespace GenericHostLab.Service
{
    public static class ServiceBaseLifetimeHostExtensions
    {
        private static IHostBuilder UseServiceBaseLifetime(this IHostBuilder hostBuilder)
        {
            return hostBuilder.ConfigureServices((hostContext, services) => services.AddSingleton<IHostLifetime, ServiceBaseLifetime>());
        }

        public static Task RunAsServiceAsync(this IHostBuilder hostBuilder, CancellationToken cancellationToken = default)
        {
            return hostBuilder.UseServiceBaseLifetime().Build().RunAsync(cancellationToken);
        }
    }
} 
其中 UseServiceBaseLifetime 將剛剛新增的 ServiceBaseLifetime 服務註冊到 DI 容器中,公開方法為 RunAsServiceAsync 提供外面使用,因此可以在 main 方法中直接使用  RunAsServiceAsync ,代碼如下
static async Task Main(string[] args)
{
    var isService = !(Debugger.IsAttached || args.ToList().Contains("--console"));

    try
    {
        var builder = CreateHostBuilder();

        if (isService)
        {
            await builder.RunAsServiceAsync();
        }
        else
        {
            await builder.RunConsoleAsync();
        }
    }
    catch (Exception e)
    {
        Console.WriteLine(e);
        throw;
    }
}
代碼中第三行是取得是否使用 debug 來執行,第七行為執行一開始介紹的 CreateHostBuilder 產生 HostBuilder,接著在根據 isService 是否為 windows service 來決定執行的方式,如果是 debug 的話會 RunConsoleAsync ,如果是透過 Service 執行則是跑自定義的擴充方法 RunAsServiceAsync。

Publish
在這一步中將透過 Visual Studio 內建的部署功能將應用程式部署到指定資料夾,在專案檔點擊右鍵按下 publish 功能,會請你選擇要發布的位置的方式是 Azure 或是 資料夾,以這次的範例中要建立 Windows Service,因此選擇指定資料夾 D:\Web\GenericHostedService
在部署的過程中也可以設定發布相關的 Release 資訊,像是 Debug 或是 Release Mode、發佈時的 ASP.NET Core 版本、Runtime 環境以及要發布到的路徑等設定,由於應用程式排程要擺放的主機 OS 環境為 Windows,因此 runtime 選擇 win64 
確認設定無誤之後,就可以按下 publish 將應用程式發布到指定的資料夾中,待發布完成之後我們可以來到指定的資料夾確認是否有發佈成功,由於剛剛所選擇的 runtime 環境是 Windows 因此可以在資料夾底下看到 ProjectName.exe 執行檔,如果環境選擇的是 Linux 則不會有 exe 只會有 .dll,這邊可以點擊 .exe 看看 Console 執行檔是否正常執行
如果發現部署的結果沒有執行檔,可以開啟專案檢查看看是否有遺漏對應的 property,在 Visual Studio 2019 快速開啟的方式是直接點專案就可以開啟 .csproj,舊的 Visual Studio 版本則是透過找到檔案後再開啟方式確認
<PropertyGroup>
  <OutputType>Exe</OutputType>
  <TargetFramework>netcoreapp2.2</TargetFramework>
  <LangVersion>7.1</LangVersion>
</PropertyGroup>

Register Windows Service
終於進入最後一個步驟,如果有從頭到尾看完的朋友辛苦了(跪拜) !! 這一步要將寫好的應用程式註冊到 Windows Service 服務,Windows Service 註冊方式在之前 註冊 Windows Service 服務 文章中有介紹過基本指令還有可以透過命令提示字元以及 powershell 兩種方式註冊,有興趣可以到文章了解這裡就不在多加描述,我們直接使用 powershell 指令來進行其註冊動作,輸入下列指令將應用程式註冊至服務中
New-Service -Name "GenericHostService" -BinaryPathName "D:\Web\GenericHostedService\GenericHostLab.exe" -DisplayName "GenericHostLab Service" -StartupType Manual -Description "This is a test service."
參數介紹


  • Name : 指定服務的名稱
  • BinaryPathName : 服務的執行檔案路徑位置
  • DisplayName : 顯示的名稱
  • StartupType : 啟動類型
  • Description : 描述
  • 建立成功之後,可以再執行指令進行啟動服務
    start-service -Name "GenericHostService""
    啟動完成就可以在 Service 裡看到剛剛開啟的服務
    另外在 2019 年 6 月 Microsoft 推出 Windows Terminal,可搭配自己喜歡的桌面在輸入指令時使用(爽度破表!!!!),這邊直接用 Windows Terminal Preview 進行輸入 powershell 指令,使用 admin 開啟 Windows Terminal Preview 軟體之後,上述指令輸入之後完整過程如下
    確認服務正常執行,正式宣告完成本次任務打完收工 !!!! 

    感想
    終於完成在 .NET Core 建立排程服務的系列文,這系列文是自己覺得近期工作上比較值得分享的項目之一,從一開始對於 .NET Core Host Service 陌生開始摸索到程式可以正常啟動,到後來深入了解與同事討論針對重要細節進行研究在重構程式碼,在完成專案後自己感覺也對 ASP.NET Core 也更進一步的認識,因此在整理成文章的過程也花了不少的時間對細節來做解釋,目的就是希望有遇到跟我相同需求的朋友可以更快地透過這系列文來上手,省下開發者踩雷到處碰壁失敗的經驗,也很感謝耐心看完的開發者朋友們,如果有更好的做法與建議歡迎提出來一起討論,最後如果有想了解更多 ASP.NET Core 的朋友推薦可以閱讀 Andrew Lock 部落格文章,相信對開發更有幫助,Happy Coding :)

    0 意見:

    張貼留言

    Copyright © m@rcus 學習筆記 | Powered by Blogger

    Design by Anders Noren | Blogger Theme by NewBloggerThemes.com