[料理佳餚] Topshelf - 一個完整接合 Windows 服務的套件

Topshelf 這個套件在網路上隨便搜尋,不管國內外都有很多文章在介紹它,我是直到最近才開始跟它有更親密的接觸,利用它我們可以很容易地將 Console 程式 Host 成 Windows 服務,變成 Windows 服務之後我們的應用程式就擁有了透明且可控的生命周期。

起手式

首先我們新增一個「主控台應用程式」的專案,接著到 NuGet 安裝 Topshelf 套件,然後呼叫 HostFactory.Run() 方法,開始一個 Hosting。

中間紅框那一大塊空白區域是我們填入相關服務配置的地方,Topshelf 提供的 API 相當完整,九成五以上的需求應該都可以滿足,我就用官方的範例來做接下來的說明。

「一般」頁籤

Windows 服務的設定本身是有 GUI 的,平常我們要對 Windows 服務做設定八成應該是透過 GUI 來做的,我這邊就用我們熟悉的 Windows 服務設定 GUI 來一一說明 Topshelf 相對應的 API,第一個遇到的是「一般」頁籤。

我們可以設定的部分為服務名稱顯示名稱描述啟動類型,程式碼如下:

var rc = HostFactory.Run(
    hc =>
        {
            // 服務名稱、顯示名稱、描述
            hc.SetServiceName("Stuff Service");
            hc.SetDisplayName("Stuff");
            hc.SetDescription("Sample Topshelf Host");

            // 啟動類型
            hc.StartAutomaticallyDelayed(); // 自動 (延遲啟動)
            hc.StartAutomatically();        // 自動
            hc.StartManually();             // 手動
            hc.Disabled();                  // 已停用
        });

服務名稱、顯示名稱、描述,除了用上述方式設定之外,還可以用 UseAssemblyInfoForServiceInfo() 方法來設定,它會去找 AssemblyInfo 當中的 Title 及 Description,把它們的值拿來用。

var rc = HostFactory.Run(
    hc =>
        {
            // 服務名稱、顯示名稱、描述
            hc.UseAssemblyInfoForServiceInfo();

            // 啟動類型
            hc.StartAutomaticallyDelayed(); // 自動 (延遲啟動)
            hc.StartAutomatically();        // 自動
            hc.StartManually();             // 手動
            hc.Disabled();                  // 已停用
        });

還有一個畫面上看不到的名稱叫做 Instance Name,如果我們要啟動兩個相同的服務,但是不同埠號,這時候我們就可以用 Instance Name 來區分。

「登入」頁籤

這個頁籤是讓我們指定服務的執行身分,除了我們自行輸入帳號及密碼之外,Topshelf 還內建了幾個常用到的執行身分:LocalSystemLocalServiceNetworkService

var rc = HostFactory.Run(
    hc =>
        {
            // 執行身分
            hc.RunAs("username or DOMAIN\\username", "password");
            hc.RunAsLocalSystem();
            hc.RunAsLocalService();
            hc.RunAsNetworkService();
            hc.RunAsPrompt();                   // 在安裝服務時才輸入帳號、密碼
            hc.RunAsVirtualServiceAccount();    // 為服務建立一個虛擬帳戶,並以此身分執行服務。
        });

「復原」頁籤

這個頁籤是讓我們設定當服務發生失敗的時候該如何處置? 使用 Topshelf 的 EnableServiceRecovery() 方法就能讓我們設定服務發生失敗時的處置動作。

var rc = HostFactory.Run(
    hc =>
        {
            // 復原選項
            hc.EnableServiceRecovery(
                sr =>
                    {
                        // 第一次失敗時(延遲 30 秒後重新啟動服務)
                        sr.RestartService(TimeSpan.FromSeconds(30));

                        // 第二次失敗時(延遲 1 分鐘後執行 notepad.exe)
                        sr.RunProgram(1, "notepad.exe");

                        // 後續失敗時(延遲 45 秒後重新啟動電腦)
                        sr.RestartComputer(TimeSpan.FromSeconds(45), "Computer is restarting.");

                        // 經過 1 天後重設失敗計數
                        sr.SetResetPeriod(1);

                        // 僅有在服務 Crash 的時候才執行失敗處置動作
                        sr.OnCrashOnly();
                    });
        });

設定失敗處置動作這邊有一個小小的限制,就是最多只能設定三個動作,而且只能按照順序去設定,也就是說不能只設定「第二次失敗時」的處置動作。

還有一個 OnCrashOnly() 方法要特別說明一下,如果看官方的說明可能會有看沒有懂,什麼是「should this be true for crashed or non-zero exits」? 這跟一個 Flag - SERVICE_FAILURE_ACTIONS_FLAG(以下簡稱 Failure Flag)有關,當 Failure Flag 為 TRUE 時,失敗處置動作在以下其中一種情況才會被執行:

  1. 服務程序終止但卻沒有回報 SERVICE_STOPPED 狀態
  2. 服務程序終止有回報 SERVICE_STOPPED 狀態,但 SERVICE_STATUS 的 ExitCode 卻不是 0。

而當 Failure Flag 為 FALSE 時,失敗處置動作僅只有在「服務程序終止但卻沒有回報 SERVICE_STOPPED 狀態」的情況下才會被執行。

那麼判斷 Failure Flag 該為 TRUE 或是 FALSE,要看 Topshelf 套件內的一個屬性 - RecoverOnCrashOnly,我爬了原始碼,當該屬性為 false 時,Failure Flag 會被改為 TRUE,但是 OnCrashOnly() 方法卻會把 RecoverOnCrashOnly 設置為 true,也就是說呼叫了 OnCrashOnly() 方法等同於 Failure Flag 為 FALSE 的情況。

如果經過以上說明還不大懂的朋友,沒關係,就直接把 OnCrashOnly() 方法加上去吧。

「相依性」頁籤

Windows 服務之間可以互相依賴,Topshelf 也替我們準備好了,除了可以自行輸入服務名稱之外,還內建了相依 EventLogIISMSSQLMSMQ 的 API。

var rc = HostFactory.Run(
    hc =>
        {
            // 相依性
            hc.DependsOn("xxx");
            hc.DependsOnEventLog();
            hc.DependsOnIis();
            hc.DependsOnMsSql();
            hc.DependsOnMsmq();
        });

掛載服務

以上這些都只是前置設定工作,最終還是要將我們的服務掛載上去,掛載服務我們就用 Service() 這個方法,然後使用 ConstructUsing() 建立實例,它有幾個事件:

  • WhenStarted
  • WhenStopped
  • WhenPaused
  • WhenContinued
  • WhenShutdown

其中 WhenStarted 及 WhenStopped 是必須要去註冊的,其他的則是選擇性的,Topshelf 只是提供一個殼,我們還是得去撰寫當這些事件發生時,服務相對應的處置。

var rc = HostFactory.Run(
    hc =>
        {
            // 開啟暫停、繼續功能。
            hc.EnablePauseAndContinue();

            // 開啟支援 Shutdown 命令
            hc.EnableShutdown();

            // Hosting Service
            hc.Service<TownCrier>(
                sc =>
                    {
                        sc.ConstructUsing(hs => new TownCrier());

                        sc.WhenStarted(s => s.Start());         // 當服務啟動時
                        sc.WhenStopped(s => s.Stop());          // 當服務停止時
                        sc.WhenPaused(s => s.Pause());          // 當服務暫停時
                        sc.WhenContinued(s => s.Continue());    // 當服務繼續時
                        sc.WhenShutdown(s => s.Shutdown());     // 當電腦關機時
                    });
        });
當我們有用到 WhenPaused、WhenContinued、WhenShutdown 時,別忘記要呼叫 EnablePauseAndContinue()EnableShutdown() 方法。

服務安裝/移除事件

服務安裝的過程也有一些事件可以註冊,總共有四個:BeforeInstallAfterInstallBeforeUninstallAfterUninstall

var rc = HostFactory.Run(
    hc =>
        {
            // 服務安裝/移除事件
            hc.BeforeInstall(ihs => { ... });
            hc.AfterInstall(ihs => { ... });
            hc.BeforeUninstall(() => { ... });
            hc.AfterUninstall(() => { ... });
        });

整合 Logger

官網的資訊來看,Topshelf 有支援 Logarylog4netNLog,我們團隊慣用的是 log4net,直接從 NuGet 下載安裝 Topshelf.Log4Net 來使用。

// 載入 log4net 設定檔
XmlConfigurator.ConfigureAndWatch(new FileInfo(configFile));

var rc = HostFactory.Run(
    hc =>
        {
            // 整合 log4net 
            hc.UseLog4Net();
        });

安裝/移除服務

我們把程式寫好了之後,現在要把它裝起來執行了,安裝/移除只能借助 Topshelf 的 Command Line,只要在執行檔後面加上參數 install 就可以安裝了。

xxx.exe install

要移除也很容易,在執行檔後面加上參數 uninstall 就是移除了。

xxx.exe uninstall

除錯

這時候大家不知道有沒有想到,那我們的程式變成 Windows 服務之後,怎麼除錯? 關於這點不用擔心,因為我們的程式本質上還是 Console 應用程式,要除錯?就按 F5 把它跑起來唄。

因此,我們的程式即使不把它變成 Windows 服務,還是能執行的。

參考資料