65.9K
CodeProject 正在变化。 阅读更多。
Home

一个启用恢复和自动启动配置的服务安装程序扩展

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.95/5 (45投票s)

2004 年 2 月 23 日

CPOL

12分钟阅读

viewsIcon

240131

downloadIcon

3168

一个扩展程序集,允许配置恢复操作的“高级”服务配置选项。

目录

本文结构如下

  1. 引言
  2. 解决方案
  3. 实现功能
  4. 整合:使用 ServiceInstallerEx 类
  5. 结论

第一部分:引言

我想先简要介绍一下服务及其功能,为接下来的解决方案提供背景。Windows 服务是 NT 操作系统(如 NT 4、Windows 2000 和 XP)的一项功能。服务在旧版“桌面”操作系统(如 Windows 95、98 和短命的 Me)上不可用。它们类似于在 UNIX 服务器和工作站上运行的守护进程。

简单来说,服务只是另一个进程,只不过它符合服务 API,允许它在后台运行,而无需用户显式启动。但请注意,单个服务进程可以运行多个服务,从而共享相同的进程空间。服务可以配置为在用户登录之前在启动时自动启动,这对于“无头”机器等需要托管独立服务器机柜中的关键任务应用程序的情况至关重要。服务的另一个有用功能是,可以使用 Windows 计算机管理控制台在本地和远程停止、暂停和恢复服务。这使系统管理员能够例如关闭和重新启动 SQL Server 数据库引擎。

在 Windows 2000 发布之前,服务在接口中没有内置容错功能。换句话说,如果服务出现故障并终止,则无法重新启动服务。实际情况是,优秀的开发人员会编写“心跳”代码,使用 Win32 进程或 PSAPI.dll 接口来确保其服务始终可用。微软 duly 注意到了这一缺陷的重要性,并引入了服务配置 API 的扩展,从而实现了精细的恢复选项。这些新扩展现在允许服务配置在服务失败时要执行的操作。这些操作包括重新启动服务、重新启动计算机、运行用户指定的命令或不执行任何操作(默认)。尽管计算机管理控制台用户界面仅允许配置和显示最多三个操作,但一个鲜为人知的事实是,服务实际上可以配置为执行任意数量的恢复操作。

Management Console Screen Shot

.NET 中的服务接口

使用 .NET Framework 中的 System.ServiceProcess 命名空间中的类,创建服务变得更加容易。其中,以下类特别值得关注:

  • System.ServiceProcess.ServiceController

    此类提供了一个丰富且易于使用的接口,用于控制本地或远程服务并获取其信息。调用者可以启动、停止或暂停服务,以及在服务上执行自定义命令。

  • System.ServiceProcess.ServiceProcessInstaller

    此类提供了一种简单的机制,可在 Windows 安装程序实用程序(如 installutil.exe)调用时安装一个或多个服务。通过在 Visual Studio .NET 中右键单击服务的设计视图并选择“添加安装程序”,即可轻松将服务进程安装程序添加到任何 Windows 服务项目中。

  • System.ServiceProcess.ServiceInstaller

    此类提供了一种安装单个服务的机制。通过在 ServiceProcessInstaller 实例中拥有多个此类实例,可以安装多个服务。ServiceInstaller 类提供特定服务的属性和配置信息。

.NET 类库中的不足

尽管有用且简单,但微软并未提供用于配置“高级”服务选项(如恢复选项)的接口。他们甚至未能提供设置服务描述的接口。我发现这有点令人失望,因为服务中的容错功能不是儿戏。服务安装的另一个不足之处是,无法将其配置为在安装后立即启动。为了启动我们的新服务,我们必须

  1. 创建一个设置项目和自定义操作来启动服务
  2. 创建一个启动进程来启动服务,并指示用户在安装后运行此进程。
  3. 指示用户重启计算机或让他们使用管理控制台(不行!)。这是不可取的,因为根本不需要重启整个计算机(可能正在运行关键任务应用程序)只是为了启动一个微小的服务。

能够将服务配置为在安装后立即启动——无需人工干预,肯定会更实用。

第二部分:解决方案

微软非常了解像我这样的人,并且做得正确,没有封闭 ServiceInstaller 类。这使我们可以通过简单地继承它并添加自己的功能来轻松扩展它。对于这个解决方案,我创建了一个名为 ServiceInstallerEx(类似于 Win32 扩展)的新类,并继承了所有现有的基类功能,例如设置依赖项、事件等。

public class ServiceInstallerEx : System.ServiceProcess.ServiceInstaller

在类的构造函数中,我只是为基类的 Committed 事件注册了两个事件处理程序。安装服务时会引发许多事件,但我们最关心的是 AfterInstallCommitted 事件。AfterInstall 事件在服务初始安装时触发。Committed 事件在服务已安装并完全提交时触发。由于我们的步骤明确修改服务配置,因此最好在服务完全提交后执行我们的脏活,而不是在 AfterInstall 事件之后执行。我们的 ServiceInstallerEx 类的构造函数注册了两个委托,以便在服务提交后执行工作。

public ServiceInstallerEx() : base()
{

  FailureActions = new ArrayList();
  base.Committed += new InstallEventHandler( this.UpdateServiceConfig );
  base.Committed += new InstallEventHandler( this.StartIfNeeded );
  logMsgBase = "ServiceInstallerEx : " + base.ServiceName + " : ";
}

UpdateServiceConfig() 方法处理服务描述和恢复操作的配置,而 StartIfNeeded() 方法根据类属性的配置来启动服务。我们的 ServiceInstallerEx 类向调用者公开了一些属性,如下所示:

  • FailureActions

    这是一个 System.Collections.ArrayList 对象,它包含 FailureAction 对象的实例。每个 FailureAction 实例代表服务失败时要执行的连续操作。例如,槽 0(第一个项目)处的项目将指示第一个失败操作,依此类推。FailureAction 对象有两个必须设置的属性:RecoverActionDelayRecoverAction 是一个枚举值,可以设置为 NoneRestartRunCommandRebootDelay 是在失败后执行配置的操作之前等待的时间(以秒为单位)。

  • 描述

    一个描述服务的字符串。

  • FailCountResetTime

    以秒为单位指定时间。如果发生故障,必须经过此时间段而没有后续故障,服务才会将故障计数重置为 0。

  • FailRebootMsg

    当重新启动计算机时,要在子网中广播的字符串。如果恢复操作指定应重新启动系统,则将广播此消息。

  • FailRunCommand

    这是一个命令行字符串,包括所有参数,这些参数将传递给 CreateProcess() 调用,当指定了运行命令操作时。

  • StartOnInstall

    这是一个布尔标志(默认 false),指示我们希望服务在安装后立即启动。如果服务依赖于其他服务,则应谨慎使用此选项。

  • StartTimeout

    这是一个等待超时值(以秒为单位,默认为 15),当 StartOnInstall 标志设置为 true 时,我们将等待服务启动。

第三部分:实现功能

advapi.dll 中的 ChangeServiceConfig2() 方法用于设置描述和故障操作。此方法基本上接受一个指向包含 SERVICE_DESCRIPTIONSERVICE_FAILURE_ACTIONS 结构的结构或结构数组的 (void *)。为了解决这两个版本,我们通过使用 DllImportAttribute 类的 EntryPoint 属性来抽象对 ChangeServiceConfig2() 的调用,如下所示:

[DllImport("advapi32.dll", EntryPoint="ChangeServiceConfig2")]
public static extern bool 
ChangeServiceFailureActions( IntPtr hService, int dwInfoLevel,
[ MarshalAs( UnmanagedType.Struct ) ] ref SERVICE_FAILURE_ACTIONS lpInfo );
 

[DllImport("advapi32.dll", EntryPoint="ChangeServiceConfig2")]
public static extern bool 
ChangeServiceDescription( IntPtr hService, int dwInfoLevel, 
[ MarshalAs( UnmanagedType.Struct ) ] ref SERVICE_DESCRIPTION lpInfo );

ServiceInstallerEx 类的 UpdateServiceConfig() 方法符合 InstallEventHandler 委托,并注册在服务提交后运行。此方法中有两个问题需要注意:

问题 1

首先,System.Runtime.InteropServices 中的 Marshal 类不提供将结构数组封送的干净方法,但提供了封送 .NET 公共类型系统中的值类型数组的方法。由于 SC_ACTION Win32 结构只是两个顺序打包的整数,因此我们实际上是通过将结构数组转换为大小加倍的整数数组来欺骗。

int[] actions = new int[numActions*2];
int currInd = 0;

foreach( FailureAction fa in FailureActions ){

  actions[currInd] = (int)fa.Type;
  actions[++currInd] = fa.Delay;
  currInd++;
}

tmpBuf = Marshal.AllocHGlobal( numActions*8 );
Marshal.Copy( actions, 0, tmpBuf, numActions*2 );

SERVICE_FAILURE_ACTIONS sfa = new SERVICE_FAILURE_ACTIONS();
sfa.lpsaActions=tmpBuf.ToInt32();
. . .

bool rslt = ChangeServiceFailureActions( svcHndl, 
                 SERVICE_CONFIG_FAILURE_ACTIONS,ref sfa );

问题 2

第二点需要注意是重启操作的问题。微软已实施了安全功能,要求代码获得特殊权限才能执行关机和重启等重大系统功能。我们用于服务配置的 ChangeServiceConfig2() 函数要求调用者在将服务故障操作设置为 Reboot 时拥有 Shutdown Privileges。为此,我们遵循 Microsoft 的指导,但使用互操作来实现。

if( needShutdownPrivilege ){

rslt = this.GrandShutdownPrivilege();
if( !rslt ) return;

}

GrantShutdownPrivilege 方法是私有范围的,调用者无法访问。它执行互操作工作,以向用户授予关机权限。此方法假定调用进程将在安装后不久终止,因此无需显式撤销权限。

我将所有有意义的代码放入一个 try-catch-finally 块中,以清理句柄、释放锁并释放内存。为了方便调试任何问题,我添加了一个简单的消息日志记录方法,用于记录到控制台和系统应用程序事件日志。

安装时启动服务

这是最简单的一步。我们在 ServiceInstallerEx 类中添加了一个 StartIfNeeded() 方法,该方法符合 InstallEventHandler 委托签名,负责在安装时启动服务。如果 StartOnInstall 属性设置为 true,则此方法使用 System.ServiceProcess 命名空间中的 ServiceController 类来启动服务。等待服务启动的超时时间也可以通过 StartTimeout 属性进行配置,该属性的默认值为 15 秒。

ServiceController sc = new ServiceController( base.ServiceName );
sc.Start();
sc.WaitForStatus( ServiceControllerStatus.Running, waitTo );

sc.Close();

第四部分:整合

ServiceInstallerEx 类由 Verifide.ServiceUtils.dll 库提供。构建此库后,使用扩展功能将非常简单,如下所示:

步骤 1:向服务项目添加安装程序

在 Visual Studio .NET 中创建 Windows 服务应用程序后,打开服务设计视图,右键单击,然后选择“添加安装程序”。这将为您的项目添加一个安装程序,并放置功能齐全的服务安装模板代码。您会注意到,为您创建了两个对象:一个 ServiceProcessInstaller 和一个 ServiceInstallerServiceProcessInstaller 类控制进程属性,例如登录帐户(我建议将帐户设置为 LocalSystem,除非另有要求)。另一方面,ServiceInstaller 实例控制诸如启动类型(自动、手动、禁用)和其他服务依赖项之类的属性。

步骤 2:使用 ServiceInstallerEx 而不是 ServiceInstaller 实例

要使用扩展类,您必须首先通过解决方案资源管理器添加对 Verifide.ServiceUtils.dll 的引用。然后,您应该包含命名空间,并使用 using 语句来使用诸如 FailureActionRecoverAction 等类的非冗长名称。

编辑在步骤 1 中创建的 ProjectInstaller.cs 文件。将代码更改为使用 Verifide.ServiceUtils.ServiceInstallerEx 类而不是 System.ServiceProcess.ServiceInstaller 类。这需要在两个地方进行更改。首先是在 ServiceInstaller 变量的声明中,其次是在“组件设计器生成的代码”区域中实例化安装程序时。值得一提的是,“组件设计器生成的代码”区域中的所有代码都是动态生成的。如果您在设计模式下打开服务安装程序,然后查看代码,您会注意到您的所有更改都将丢失。因此,在此区域中除了进行必要的类型更改外,不要做任何其他事情。(设计视图生成的代码不会更改类型。)

public class ProjectInstaller : System.Configuration.Install.Installer
{

  private System.ServiceProcess.ServiceProcessInstaller 
                                    serviceProcessInstaller1;
  private Verifide.ServiceUtils.ServiceInstallerEx serviceInstaller1;
.
#region Component Designer generated code

  private void InitializeComponent()
  {
    this.serviceProcessInstaller1 = 
        new System.ServiceProcess.ServiceProcessInstaller();
    this.serviceInstaller1 = new Verifide.ServiceUtils.ServiceInstallerEx();

步骤 3:没有步骤三

好吧,我撒谎了,有一个,而且很简单。在 ProjectInstaller 类的构造函数中添加代码来配置选项,这样就完成了。以下是用户需要做的**所有**事情。当然,如果您选择不设置任何扩展属性,则无需执行任何其他操作;由于我们继承自 ServiceInstaller 类,因此我们非常透明。

public ProjectInstaller()
{
 InitializeComponent();

 serviceInstaller1.Description = "Look Smeagol! Its the Precious!";   
 serviceInstaller1.FailCountResetTime = 60*60*24*4;
 serviceInstaller1.FailRebootMsg = "Whitney Houston! We have a problem" ;

 serviceInstaller1.FailureActions.Add( 
   new FailureAction( RecoverAction.Restart,60000)  );
 serviceInstaller1.FailureActions.Add( 
   new FailureAction( RecoverAction.RunCommand, 2000 ) );
 serviceInstaller1.FailureActions.Add( 
   new FailureAction( RecoverAction.None, 3000 ) );

 serviceInstaller1.StartOnInstall = true;
  
}

步骤 4:安装服务

从您生成的服务应用程序的 bin 目录中,使用 installutil.exe 命令安装服务。从 Visual Studio .NET 命令提示符(开始 -> 程序 -> Visual Studio -> 工具 -> 命令提示符)运行以下命令,以确保 installutil 程序在您的路径中。

安装服务:installutil servicename.exe

卸载:installutil /u servicename.exe

请注意,您必须指定服务可执行文件的完整路径,或者您必须位于服务所在的目录中,才能运行 installutil 程序。转到 Windows 管理控制台(右键单击“我的电脑”->“管理”->“服务和应用程序”->“服务”)。要查看恢复选项,请在列表中右键单击该服务,选择“属性”,然后选择“恢复”选项卡。

第五部分:结论

在使用 .NET 两年多之后,我得出的结论是,该平台确实使许多任务变得非常容易。但是,框架并未公开许多精细的功能。对于那些像我一样,敢于说,在传统级别上编程的人来说,“高级”功能并不可怕。我们已经做过,最重要的是,我们了解它们及其作用。

服务是分布式和业务平台开发不可或缺的一部分。在我看来,微软在简化创建方面做得很好,更重要的是简化了服务的管理。然而,根据我的经验,Windows 2000 在恢复选项方面的进步使我个人能够获得许多安眠之夜。我非常失望,因为服务中的容错功能在框架类库的开发中并非优先事项。

在实现解决方案时,我选择提供最简单的接口,以允许开发人员保持与 FCL 名称和用法的兼容性。它依赖于 Win32 API 来填补 FCL 的空白。啊,Win32,那个老式的、高性能的、低碳的、低脂的、赖以为生的东西,让我摆脱了许多困境。Win32 互操作是获得对“高级”功能精细控制的绝佳机制,但必须谨慎使用。微软最终将淘汰此 API(尽管他们可能会在内部使用它),随之而来的是未包含在 .NET FCL 中的大量功能将永远消失。

© . All rights reserved.