更上层楼:动态安装你的windows服务

前言:先说明一下本文示例windows服务的简单需求,即根据外部配置实现不同方式记录日志的功能。记录日志的方式有三种,分为文本记录、数据库记录以及文本和数据库同时记录日志。如您所知,这个功能基本上没有任何实用价值,纯粹为了方便本文的举例和说明。文章最后提供示例demo下载。

一、中规中矩,写一个简单的windows服务

1、新建windows服务

打开开发神器VS(我这里用的是VS2010),单击“新建项目”,在弹出的选项卡上左侧选择“Windows”,然后在右侧选择“Windows服务“模板,确定即可。按照命名需要,本文示例中我把VS默认生成的Service1重命名为LogService。

(1)、构造函数

        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操作系统的服务控制器上,我们查看任意一个服务的属性,肯定会看到”启动“、”停止“、”暂停“和”恢复“四个按钮选项。上面的四个重写方法我们可以理解成就是让我们实现某个服务的四个按钮选项下的对应事件。

SimpeLogervice

需要说明的是,windows服务都是在后台默默无闻地低调工作着,所以对开发人员来讲,通常长时间大批量的后台工作任务,做成windows服务再合适不过。但是如果您的程序实现使用了异步,就会给服务的停止、暂停和恢复等控制带来极大难度,而且有时候甚至会产生意想不到的结果。本文示例中对于停止、暂停和恢复,都是对一个静态线程进行操作。实际开发中这种方式并不保险,因为异步程序中你实在不好控制程序到底执行到哪一步,执行的结果怎么样。我估计微软默认不生成暂停和恢复这两个事件,也是基于控制不易方面的考虑。在实际项目开发中,除非可以明确确定异步程序已经暂时不工作(通过查看特定日志),否则“暂停”和“恢复”这两个按钮通常默认都是不可用的(CanPauseAndContinue = false)。

(3)、服务里的主要业务逻辑简单说明

在LogBuilder类里,已经封装了该windows服务主要的业务逻辑。其中三个方法Log2Text,Log2DB和LogBoth看命名知道是什么意思了。本文重点也不在这里,这里一带而过。

2、为服务添加Installer

服务的主体实现已经有了,当然还需要服务安装程序逻辑。打开LogService设计界面,右键选择”Add Installer“栏目,在生成的ProjectInstaller里就轻松添加了一个ServiceInstaller和ServiceProcessInstaller实例。这里你可以根据VS提供的可视化的方式给两个Installer进行属性设置,也可以直接在构造函数中设定。

   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事件,再通过下面的方法:

    /// <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的完整的路径。在它内部的源码中,我们看到:

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

也就是要获取.net framework的安装路径,然后根据它间接找到installutil工具.exe文件:

            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

posted on 2011-03-12 14:09  JeffWong  阅读(6096)  评论(10编辑  收藏  举报