关于使用BCB6编写Windows服务的问题

 前日因为系统遗留问题,不得不重新打开已经N久没有使用的Borland C++ Builder 6,编写一个Windows服务。最初设想是编写一个能够根据指定参数,设置诸如服务名称、显示名称、描述、配置文件路径的东西,以一个服务程序作为多种不同服务内容的外壳,能够在Windows的服务管理器中分别控制。由于BCB的服务模板未考虑定制的情况,所以需要费点周折。

 

BCB中用户实现的服务对象继承自TService,这里暂定为TMyService,当使用全局对象Svrmgr::Application(TServiceApplication)来创建服务对象时,用户的服务对象自动成为Application的组件之一。

通常这样来创建服务对象:

  1. Application->CreateForm(__classid(TMyService), &MyService);

BCB的SvrMgr.hpp其实是Delphi实现的申明。查看SvrMgr.pas,可以见到创建服务的代码:

  1. Svc := CreateService( SvcMgr, 
  2.                       PChar(Name), //< TMyService->Name用作服务名称
  3.                       PChar(DisplayName), //< TMyService->DisplayName用作显示名称
  4.                       SERVICE_ALL_ACCESS,
  5.                       GetNTServiceType,
  6.                       GetNTStartType,
  7.                       GetNTErrorSeverity,
  8.                       PChar(Path),
  9.                       PChar(LoadGroup),
  10.                       PTag,
  11.                       PChar(GetNTDependencies),
  12.                       PSSN,
  13.                       PChar(Password));

这里可以看到,当使用/install参数来注册服务时,BCB的实现是使用TMyService的Name属性作为服务名称,DisplayName作为服务显示名称。如果要定制这两个名称,我们可以增加额外的参数,设为/name和/displayname,分别用于指定服务名称和服务显示名称。示例如下:

 

MyService /install /name:"ThisIsAnotherService" /displayname:"显示名称"

 

/name和/displayname后面跟一个":"来分隔后继的字符串,在参数解析时,系统会将双引号中的字符串作为一个整体对待。不了解这一点和对引号有不同看法的请参考Windows提供的帮助:命令提示符。

 

BCB只处理/install和/uninstall参数,增加的参数需要自己处理,这里要用到两个BCB提供的函数:ParamCount()和ParamStr(),具体使用方法请参考BCB的Help。如下是示例代码:

 

 

 

  1. bool isInstall = FindCmdLineSwitch("install"true); // 安装服务标志
  2. bool isUninstall = FindCmdLineSwitch("uninstall"true); // 卸载服务标志
  3. AnsiString serviceName; // 服务名称
  4. AnsiString serviceDispName; // 服务显示名称
  5. if (ParamCount() > 1 && (isInstall || isUninstall)) // 指定了参数以及安装、卸载标志
  6. {
  7.     // 先创建之
  8.     Application->CreateForm(__classid(TMyService), &MyService);
  9.     if ( isInstall || isUninstall ) // 对于安装或卸载情况
  10.     {
  11.         for(int i = 1; i <= ParamCount(); i++) // 循环处理所有参数
  12.         {
  13.             AnsiString param = ParamStr(i).LowerCase(); // 为便于比较,先转换成小写
  14.             int pos; // 参数位置
  15.             if (param.Pos("/name:")) // 匹配服务名称: "/name:xxxxx"
  16.             {
  17.                 pos = param.Pos(":");
  18.                 serviceName = param.SubString(pos + 1, param.Length() - pos);
  19.                 if ((pos = serviceName.Pos(" ")) > 0) // 删除空格,这个其实不必要,系统会指出名称非法
  20.                 {
  21.                     serviceName.Delete(pos, 1);
  22.                 }
  23.             }
  24.             else
  25.             if (param.Pos("/dispname:")) // 匹配服务显示名称: "/serviceDispName:xxxxx"
  26.             {
  27.                 pos = param.Pos(":");
  28.                 serviceDispName = param.SubString(pos + 1, param.Length() - pos);
  29.             }
  30.         }
  31.         if (serviceName.Length() > 0)
  32.             Application->Components[0]->Name = serviceName; // 使用指定的服务名称
  33.         else
  34.             serviceName = Application->Components[0]->Name; // 未指定服务名称,会使用TMyService的默认名称:MyService
  35.         if (serviceDispName.Length() == 0)
  36.             serviceDispName = serviceName;  // 如果未指定显示名称,使用服务名称来代替
  37.             
  38.         ((TService *)Application->Components[0])->DisplayName = serviceDispName; // 设置显示名称
  39.     }
  40. }
  41. Application->Run(); //< 服务开始运行

现在,编译并注册我们的服务,可以看到它按指定的方式显示在服务列表中,让我们试着启动它。。。。。。等等。。。。。启动失败!!!!OOOOOOOH! SHIT!!!!!

点解!?!?

 

再来查看SvrMgr.pas,BCB(或Delphi?)是这样启动服务滴:

 

 

 

  1. // .....
  2. begin
  3.     Forms.Application.OnException := OnExceptionHandler; // 异常句柄
  4.     ServiceCount := 0// 服务对象(TXXXService)数量
  5.     for i := 0 to ComponentCount - 1 do // Application包含的组件数量即服务对象数量
  6.       if Components[i] is TService then Inc(ServiceCount);
  7.     SetLength(ServiceStartTable, ServiceCount + 1); // 设置服务启动表的尺寸
  8.     FillChar(ServiceStartTable[0], SizeOf(TServiceTableEntry) * (ServiceCount + 1), 0); // 清零
  9.     J := 0;
  10.     for i := 0 to ComponentCount - 1 do // 填充服务入口表
  11.       if Components[i] is TService then
  12.       begin
  13.         ServiceStartTable[J].lpServiceName := PChar(Components[i].Name); // 这里使用的是Name属性!
  14.         ServiceStartTable[J].lpServiceProc := @ServiceMain; // 关于ServiceMain入口函数,请参阅Windows SDK help
  15.         Inc(J);
  16.       end;
  17.     StartThread := TServiceStartThread.Create(ServiceStartTable); // 启动服务
  18.     // .....
  19. // TServiceStartThread的线程函数实现
  20. procedure TServiceStartThread.Execute;
  21. begin
  22.   if StartServiceCtrlDispatcher(FServiceStartTable[0]) then // 使用服务入口表启动服务
  23.     ReturnValue := 0
  24.   else
  25.     ReturnValue := GetLastError;
  26. end;

Windows API StartServiceCtrlDispatcher()使用服务入口表启动服务,该操作是名称相关的,而我们已经使用了指定的名称安装服务,所以当系统(TServiceApplication)使用原有的名称(这里是MyService)来启动服务时,会找不到名为MyService的服务(我们已经指定其名称为ThisIsAnotherService),导致启动失败。由于系统在启动服务时,没有提供关于服务名称的上下文,因此我们需要作一点手脚,创造这个上下文。简单的方法是:在安装服务时,修改服务的启动路径记录,添加服务名称作为参数。在服务启动时,解析这个参数,并使用该参数修改TMyService->Name,这样服务应能顺利启动。代码示例如下:

 

先添加额外的参数,保存服务名称:

 

  1. // .......
  2. Application->Run();
  3. // 需要在Run以后,此时服务已经成功安装
  4. if (isInstall)
  5. {
  6.     // 最简单的方法是Hack注册表
  7.     TRegistry* reg = new TRegistry();
  8.     AnsiString key = "//System//CurrentControlSet//Services//" + serviceName; // 注册表路径
  9.     reg->RootKey = HKEY_LOCAL_MACHINE;
  10.     if (reg->OpenKey(key, false)) // 打开荐
  11.     {
  12.         AnsiString imagePath = reg->ReadString("ImagePath"); // 介就系程序映像的路径
  13.         reg->WriteString("ImagePath", imagePath + " -" + serviceName); // 我们要做的是添加额外的参数:服务名称
  14.     }
  15.     reg->CloseKey();
  16.     delete reg;
  17. // if isInstall then hack registry

再添加对参数的处理:

  1. if (paramCount() > 1 && (isInstall || isUninstall))
  2. {
  3.     //......
  4. }
  5. else // 非安装,即启动
  6. {
  7.     // 检查参数个数
  8.     if (ParamCount() > 0)
  9.     {
  10.         AnsiString extraParam = ParamStr(1).LowerCase(); // 额外参数,转换为小写
  11.         AnsiString specifiedServiceName;
  12.         if (extraParam.Pos("-")) // 定位"-"
  13.         {
  14.             int pos = extraParam.Pos("-");
  15.             specifiedServiceName = extraParam.SubString(pos + 1, extraParam.Length() - pos); // 解析服务名称
  16.         }
  17.         if (!specifiedServiceName.Length()) // 如果名称无效,则使用默认名称:MyService
  18.         {
  19.             Application->CreateForm(__classid(TMyService), &MyService); // 使用默认名称
  20.         }
  21.         else
  22.         {
  23.             Application->CreateForm(__classid(TMyService), &MyService);
  24.             MyService->Name = specifiedServiceName; // 使用指定的名称
  25.         }
  26.     } // 
  27. // if ... else
  28. // ...
  29. Application->Run();
  30. // ...

 

现在,编译程序,重新安装服务,再试着启动一下。。。。。。

如果没有错误的话,服务应该能够顺利启动。(废话)

 

BCB没有提供服务的描述属性,这也可以通过修改注册表的方法实现,操作很简单,这里不在多言。

 

此乃末技。。。。。

 

你可能感兴趣的:(windows,service,application,Delphi,Borland,Components)