更上層樓:動態安裝你的windows服務

更上層樓:動態安裝你的windows服務

前言:先說明一下本文示例windows服務的簡單需求,即根據外部配置實現不同方式記錄日誌的功能。記錄日誌的方式有三種,分爲文本記錄、數據庫記錄以及文本和數據庫同時記錄日誌。如您所知,這個功能基本上沒有任何實用價值,純粹爲了方便本文的舉例和說明。文章最後提供示例demo下載。

一、中規中矩,寫一個簡單的windows服務

1、新建windows服務

打開開發神器VS(我這裏用的是VS2010),單擊“新建項目”,在彈出的選項卡上左側選擇“Windows”,然後在右側選擇“Windows服務“模板,確定即可。按照命名需要,本文示例中我把VS默認生成的Service1重命名爲LogService。

(1)、構造函數

1
2
3
4
5
6
7
8
9
public LogService()
{
    InitializeComponent();
    this.ServiceName = "SimpleLogService";
    CanPauseAndContinue = true;
    CanStop = true;
    CanShutdown = true;
    CanHandleSessionChangeEvent = true;
}

我們在構造函數裏設置了幾個常用的屬性。其中CanPauseAndContinue = true標識該windows服務可以暫停和繼續。其實我們也可以在設計界面進行屬性設置,不是重點,略過。

(2)、重寫事件

默認情況下,在LogService類中VS已經替我們生成了OnStart和OnStop方法。如果我們還設置了屬性 CanPauseAndContinue = true, 則我們可能還要重寫OnPause和OnContinue方法。在windows操作系統的服務控制器上,我們查看任意一個服務的屬性,肯定會看到”啓動“、”停止“、”暫停“和”恢復“四個按鈕選項。上面的四個重寫方法我們可以理解成就是讓我們實現某個服務的四個按鈕選項下的對應事件。

        /// <summary>
        /// start
        /// </summary>
        /// <param name="args"></param>
        protected override void OnStart(string[] args)
        {
            Logger.AppendLog("服務啓動", LogBuilder.logDir);
            LogBuilder.Start();
        }

        /// <summary>
        /// stop
        /// </summary>
        protected override void OnStop()
        {
            try
            {
                if (LogBuilder.dictWorkThread == null)
                {
                    return;
                }
                foreach (KeyValuePair<string, Thread> item in LogBuilder.dictWorkThread)
                {
                    if (item.Value == null)
                    {
                        continue;
                    }
                    item.Value.Abort();
                    Logger.AppendLog(string.Format("{0}線程已經終止", item.Value.Name), LogBuilder.logDir);
                }

            }
            finally
            {
                base.OnStop();
            }
        }

        /// <summary>
        /// pause
        /// </summary>
        protected override void OnPause()
        {
            try
            {
                if (LogBuilder.dictWorkThread == null)
                {
                    return;
                }
                foreach (KeyValuePair<string, Thread> item in LogBuilder.dictWorkThread)
                {
                    if (item.Value == null || item.Value.IsAlive == false)
                    {
                        continue;
                    }
                    if ((item.Value.ThreadState & (System.Threading.ThreadState.Suspended | System.Threading.ThreadState.SuspendRequested)) > 0)
                    {
                        Logger.AppendLog("線程暫停!" + item.Value.ThreadState, LogBuilder.logDir);
                        continue;
                    }
                    item.Value.Suspend();//線程掛起
                    Logger.AppendLog(string.Format("{0}線程已經暫停工作", item.Value.Name), LogBuilder.logDir);
                }
            }
            finally
            {
                base.OnPause();
            }
        }

        /// <summary>
        /// continue
        /// </summary>
        protected override void OnContinue()
        {
            try
            {
                if (LogBuilder.dictWorkThread == null)
                {
                    return;
                }
                foreach (KeyValuePair<string, Thread> item in LogBuilder.dictWorkThread)
                {
                    if (item.Value == null || item.Value.IsAlive == false)
                    {
                        continue;
                    }
                    if ((item.Value.ThreadState & (System.Threading.ThreadState.Suspended | System.Threading.ThreadState.SuspendRequested)) == 0)
                    {
                        continue;
                    }
                    item.Value.Resume();//繼續已經掛起的線程
                    Logger.AppendLog(string.Format("{0}線程已經開始繼續工作", item.Value.Name), LogBuilder.logDir);
                }
            }
            finally
            {
                base.OnContinue();
            }
        }

需要說明的是,windows服務都是在後臺默默無聞地低調工作着,所以對開發人員來講,通常長時間大批量的後臺工作任務,做成windows服務再合適不過。但是如果您的程序實現使用了異步,就會給服務的停止、暫停和恢復等控制帶來極大難度,而且有時候甚至會產生意想不到的結果。本文示例中對於停止、暫停和恢復,都是對一個靜態線程進行操作。實際開發中這種方式並不保險,因爲異步程序中你實在不好控制程序到底執行到哪一步,執行的結果怎麼樣。我估計微軟默認不生成暫停和恢復這兩個事件,也是基於控制不易方面的考慮。在實際項目開發中,除非可以明確確定異步程序已經暫時不工作(通過查看特定日誌),否則“暫停”和“恢復”這兩個按鈕通常默認都是不可用的(CanPauseAndContinue = false)。

(3)、服務裏的主要業務邏輯簡單說明

在LogBuilder類裏,已經封裝了該windows服務主要的業務邏輯。其中三個方法Log2Text,Log2DB和LogBoth看命名知道是什麼意思了。本文重點也不在這裏,這裏一帶而過。

2、爲服務添加Installer

服務的主體實現已經有了,當然還需要服務安裝程序邏輯。打開LogService設計界面,右鍵選擇”Add Installer“欄目,在生成的ProjectInstaller裏就輕鬆添加了一個ServiceInstaller和ServiceProcessInstaller實例。這裏你可以根據VS提供的可視化的方式給兩個Installer進行屬性設置,也可以直接在構造函數中設定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public ProjectInstaller()
     {
         InitializeComponent();
         ServiceInstaller installer = new ServiceInstaller();
         installer.ServiceName = "SimpleLogService";//服務的名稱要和LogService構造函數裏的服務名稱一致
         installer.DisplayName = "測試日誌記錄Windows服務";//windows服務顯示的名稱
         installer.Description = "這是一個簡單的測試日誌記錄Windows服務,在log文件夾下可以看到詳細文本日誌";
         installer.StartType = ServiceStartMode.Manual; // 自動 手動 或禁用 這裏設爲手動
 
         ServiceProcessInstaller processInstaller = new ServiceProcessInstaller();
         // 採用本地系統帳戶運行服務
         processInstaller.Account = System.ServiceProcess.ServiceAccount.LocalSystem;
         processInstaller.Username = null;
         processInstaller.Password = null;
         this.Installers.AddRange(new System.Configuration.Install.Installer[] { installer, processInstaller });
     }

如上所示,ServiceInstaller對象可以讓我們設置服務的服務名,顯示名,簡介以及啓動方式等等; 而ServiceProcessInstaller對象實例可以讓我們設置運行該服務的賬戶類型,用戶名和密碼等等。

上面列舉的屬性都是開發中常用的,我們清楚它們的含義知道如何設置即可。其中Windows服務的服務名稱(ServiceName)屬性極其重要,該屬性是服務的惟一的不可重複的名稱,我們可以簡單地理解成它是衆多服務的唯一標識符。關於如何啓動和停止服務,大家可能都知道,我們可以在命令行中使用“net start servicename”命令來啓動服務,使用“net stop servicename”來停止服務。

二、亦步亦趨,認識下installutil工具

關於安裝和卸載windows服務,您可以使用微軟提供的installutil工具,通過命令行的方式實現安裝和卸載。

安裝:installutil myAssembly.exe

卸載:installutil /u myAssembly.exe

還有一種經常使用的方式,就是像田志良童鞋的這篇用C#實現通用守護進程裏介紹的“安裝注意事項”一樣,通過已經生成好的批處理命令文檔進行安裝和卸載。

當然您也可以按常規方式給windows服務打包進行安裝和卸載,一如園子裏RyanDing寫的.NET4 Windows Service 監控磁盤文件最後介紹的那樣。

在本文測試的示例中,服務安裝運行結果,依照慣例,當然有圖有真相:

SimpleLogService2

到這裏,初步寫個windows服務的基礎知識和準備工作已經介紹完了,按照我們的說明,好像不論開發、安裝還是卸載都很簡單,事實也確實如此,平時開發中我們可能會把重心更多地放在windows服務的業務邏輯上。需要注意的是,這裏示例安裝的是標準的windows服務應用程序,其實對於控制檯應用程序甚至winform應用程序我們同樣也可以作爲windows服務的安裝媒介,只要想方設法讓你的服務Run起來即可(本文示例代碼就是在Main函數中調用“System.ServiceProcess.ServiceBase.Run( windows服務實例 )”來運行服務),最多也只是表現形式的不同,原理上沒有任何區別。

三、”用一種聰明的方法替代msi安裝包來安裝windows服務“

這裏的小標題顯得怪怪的,不符合我以往一貫的淺談和總結的風格,原因很簡單,這一節主要是參考code project上的Install a Windows Service in a smart way instead of using the Windows Installer MSI package而寫成的,標題是直譯過來的,內容個人認爲也還算名副其實,算是對本文大標題的一點補充。

前面提到要想方設法讓服務Run起來,這裏就簡單介紹一下DynamicInstaller,實際上它的基本原理很簡單,也就是間接通過installutil工具實現安裝和卸載。

1、命令行啊命令行

有很多開發人員都喜歡使用命令行的方式工作,而且很多時候確實挺方便也挺快捷的。在圖形用戶界面(GUI)大行其道的當今之世,使用命令行(CLI)固然讓你顯得另類或者極客,但是有習慣就有不習慣的,有喜歡就有不喜歡的,我們很多人可能更接受眼見爲實簡單直觀的GUI(很顯然,我很不熟悉CLI而更依賴於GUI)。偏偏微軟經常“偷懶”,它提供的很多工具比如wsdl、iisadmin等和開發密切相關的幾個重要工具就要通過命令行才能搞定(我還記得當初在大學裏第一次在電腦上安裝個iis信息服務,花了不少時間弄得暈頭轉向),而installutil也不例外,必須通過命令行設置參數來進行windows服務的安裝和卸載(同時我也想到微軟自己的反彙編工具ILDASM,在對待這個工具的問題上,微軟表現得很勤快,這個玩意玩起來倒不是通過命令行,但是它卻完全被Reflector這個神器搶了風頭)。

我們完全可以通過簡單直觀的GUI的方式進行windows服務的安裝和卸載,不過不是像傳統的msi安裝包一樣一路next下來,雖然原理上它們都是間接地通過installutil這個工具。下面我就參考英文原文,主要重點介紹一下自己對DynamicInstaller的簡單開發使用和我對源碼實現的一些粗淺理解:
(1)、解決方案
DynamicApp
解決方案項目劃分很明確,其中SimpleLogService就是我們上面中規中矩寫過的window服務,和這裏介紹的DynamicInstaller沒有關係,可以忽略;
在DotNet.Common.Util非常簡單,只有一個Logger類用來記錄文本日誌;
DotNet.Common.WinSvcInstaller項目中,我重新改進並封裝了一下codeproject原文裏的DynamicInstaller。這樣以後需要寫windows服務,我們都可以引用該程序集實現“動態”安裝,而且可以多次複用。

在DynamicInstallTool項目中我還做了一個簡單的winform小程序,引用DotNet.Common.WinSvcInstaller程序集,以後使用這個小程序的時候,任何安裝程序(ProjectInstaller)繼承自DotNet.Common.WinSvcInstaller中的DynamicInstaller的windows服務程序都可以利用DynamicInstallTool實現可視化安裝或卸載。
SimpleWinService項目幾乎就是SimpleLogService的拷貝,唯一不同的地方是它引用了DotNet.Common.WinSvcInstaller,它的ProjectInstaller繼承自DynamicInstaller,毫無疑問,它可以讓我們使用DynamicInstaller進行動態安裝和卸載。

最終結果如下圖所示,直觀的GUI界面,雖然簡陋,但是可以很輕易上手:

DynamicInstaller

想要安裝或者卸載,只要選擇你的服務可執行文件,填寫你想要設置的幾個參數就可以了。示例寫得很簡陋,當然,你完全可以按照原文中提供的網絡安裝方法,實現一個更通用的網絡安裝工具。

(2)、源碼簡析

如果您看完了英文原文的介紹,應該已經瞭解了DynamicInstaller的基本工作原理。說說我所理解的幾個關鍵點:

a、服務參數接收
通常我們的windows服務在源碼裏很多屬性和參數都是設定寫死的。如何讓我們的參數動態傳入呢?DynamicInstaller類的源碼告訴我們,通過重寫System.Configuration.Install.Installer的OnBeforeInstall事件和OnBeforeUninstall事件,再通過下面的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/// <summary>
    /// Return the value of the parameter in dicated by key
    /// </summary>
    /// <PARAM name="key">Context parameter key</PARAM>
    /// <returns>Context parameter specified by key</returns>
    public string GetContextParameter(string key)
    {
        string result = string.Empty;
        if (this.Context.Parameters.ContainsKey(key) == true)
        {
            result = this.Context.Parameters[key];
        }
        return result;
    }

這裏的Context(上下文)是一個InstallContext對象實例,它主要包含了關於當前安裝的信息,不妨和我們熟悉的HttpContext比較一番。繼續說GetContextParameter這個方法,按照相關鍵值關係,我們可以分別接收到外部傳遞給服務的相關參數,然後設置對應的屬性,這就比那些寫死的方法明顯高明不少。比如我們要卸載某一服務,必須傳入服務名,在OnBeforeUninstall卸載事件中,我們可以通過下面的重寫事件實現服務名的獲取:

OnBeforeUninstall

b、服務參數傳遞

既然有參數接收,那麼必有傳遞。如何傳參呢?我們來剖析一下WindowsServiceInstallUtil類。WindowsServiceInstallUtil這個類非常關鍵,傳參和參數組合的過程幾乎全部交給它忙活了。
既然我們的原理是要間接通過installutil來安裝和卸載,必須要找到installutil在哪裏,即需要首先找到微軟.net框架所帶的installutil.exe的完整的路徑。在它內部的源碼中,我們看到:

1
public static string InstallUtilPath = System.Runtime.InteropServices.RuntimeEnvironment.GetRuntimeDirectory(); //@"C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727";

也就是要獲取.net framework的安裝路徑,然後根據它間接找到installutil工具.exe文件:

1
proc.StartInfo.FileName = Path.Combine(InstallUtilPath, "installutil.exe");

根據我的測試,它返回的是.net framework的最新版本安裝路徑。其實要獲取.net framework的安裝路徑,還有其他方法,這裏不做介紹了。

好。installutil工具路徑終於找到了,那麼接着如何讓這個工具啓動並工作(安裝)呢(注:這裏只是對於安裝而言,卸載的和安裝原理一致,不做太多解釋說明了)?繼續分析源碼,看到下面的CallInstallUtil方法:

CallInstallUtil

看命名就知道這明明就是在召喚安裝命令嘛。令人眼前一亮的是我們看到了讓人熟悉的Process類,這個Process類是DynamicInstaller的一個重要救星。
好,很好。這個Process類的作用,大家看名稱就應該已經知道它主要是幹什麼用的了,微軟的MSDN上也有明確的示例和說明。通過它,我們可以單獨啓動很多應用程序,比如命令提示符工具cmd,ms的反彙編工具ildasm等等等等,言歸正傳,不多做介紹。這裏我們完全可以簡單地理解成,通過Process開啓一個進程,啓動installutil工具,通過Process實例的StartInfo.Arguments 設定所需外部參數。嗯,設定外部所需的參數,那麼……
參數,你在哪裏?在哪裏?你可知道…
我們看到CallInstallUtil函數傳了個字符串installUtilArguments,而且是通過proc.StartInfo.Arguments = installUtilArguments這樣賦值的,毫無疑問,這個字符串就是外部各個參數拼接出來的。然後我們找啊找啊找,終於不費力就找到了,安裝參數的拼接是通過GenerateInstallutilInstallArgs函數實現的:

GenerateInstallutilInstallArgs

好,很好,非常好。我們驚喜地發現我們平時開發中熟悉的常用的windows服務和安裝屬性盡收眼底。接着分析,我們看到函數裏通過_wsInstallInfo對象變量的各種屬性進行不同參數的拼接。那麼如何設置_wsInstallInfo變量的屬性呢?看源碼,我們又看到了WindowsServiceInstallInfo類,WindowsServiceInstallUtil內部的WindowsServiceInstallInfo屬性和_wsInstallInfo變量。我kao,不就是在外面的應用程序中new一個WindowsServiceInstallInfo對象設置設置屬性,再new一個WindowsServiceInstallUtil對象再將前面設置好屬性的WindowsServiceInstallInfo對象傳遞給它的WindowsServiceInstallInfo屬性那麼簡單嘛(可以參考DynamicInstallTool的簡單實現)。不再多分析了,大功告成,over。

到這裏你可能還會猜疑說,這個也不過如此嘛,也就是間接利用了installutil工具而已,甚至還不如installutil來得簡潔呢。先別急,在下面介紹的場景裏這個DynamicInstaller可能才能真正發揮優勢。

2、相同的一份服務程序,多個服務命名

如果我們現在又有一個看似不太合理的要求,即想把簡單日誌服務(SimpleWinService)配置成文本記錄日誌的方式(LogType爲Text),在本地安裝兩個實例,功能一模一樣,但是服務名稱不一樣。你可能會說這麼做成兩個服務也沒什麼意義啊,難道你想讓你的日誌記錄的更多更快一點?這裏不去討論這種需求場景的現實意義(實際應用中想讓後臺程序處理的更快更多一點,多安裝幾個實例還是很現實的解決途徑的,可以看成是多實例互不干擾地並行工作),要實現這種功能,傳統的做法,我們只要妥協一下,方法笨就笨一點,複製一份相同的代碼,程序裏命名不同服務名,通過installutil命令行方式安裝兩次完事。但是,要兩份程序外加改命名,感覺有點破費了,而且如果需要安裝的實例再多幾個,感覺就有點那什麼了。通過DynamicInstaller,我們可以輕而易舉地實現一個服務(程序)多次命名,安裝不同服務名稱的實例。通過上面剛剛介紹過的這個簡單工具(DynamicInstallTool),就可以實現一份相同的代碼,但是安裝成兩個不同名稱的服務實例。

我們按照上面介紹的方法,使用那個DynamicInstallTool工具,一步一步安裝(同樣道理您也可以卸載)兩個(甚至多個)實例的windows服務,本文示例我安裝了兩次簡單日誌服務,服務命名分別是LogTextService1和LogTextService2,可以在服務控制器上查看到這兩個服務:

logservice

您可以下載源碼在本地測試運行一下試試看。

您也可以通過下面的命令一目瞭然地查看某一個windows服務的具體信息:

wmic service where name='winsvcname' get /value


寫個windows服務並不難,不知您有沒有什麼好的開發心得?不妨說出來大家共同學習討論一下。期待您的更好意見和建議。

其他參考:

http://msdn.microsoft.com/zh-cn/library/system.serviceprocess(v=VS.80).aspx

示例下載:DynamicInstallerApp


發佈了0 篇原創文章 · 獲贊 3 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章