前日因為系統遺留問題,不得不重新開啟已經N久沒有使用的Borland C++ Builder 6,編寫一個Windows服務。最初設想是編寫一個能夠根據指定參數,設定諸如服務名稱、顯示名稱、描述、設定檔路徑的東西,以一個服務程式作為多種不同服務內容的外殼,能夠在Windows的服務管理員中分別控制。由於BCB的服務範本未考慮定製的情況,所以需要費點周折。
BCB中使用者實現的服務物件繼承自TService,這裡暫訂為TMyService,當使用全域對象Svrmgr::Application(TServiceApplication)來建立服務物件時,使用者的服務物件自動成為Application的組件之一。
通常這樣來建立服務物件:
- Application->CreateForm(__classid(TMyService), &MyService);
BCB的SvrMgr.hpp其實是Delphi實現的申明。查看SvrMgr.pas,可以見到建立服務的代碼:
- Svc := CreateService( SvcMgr,
- PChar(Name), //< TMyService->Name用作服務名稱
- PChar(DisplayName), //< TMyService->DisplayName用作顯示名稱
- SERVICE_ALL_ACCESS,
- GetNTServiceType,
- GetNTStartType,
- GetNTErrorSeverity,
- PChar(Path),
- PChar(LoadGroup),
- PTag,
- PChar(GetNTDependencies),
- PSSN,
- 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。如下是範例程式碼:
- bool isInstall = FindCmdLineSwitch("install", true); // 安裝服務標章
- bool isUninstall = FindCmdLineSwitch("uninstall", true); // 卸載服務標章
- AnsiString serviceName; // 服務名稱
- AnsiString serviceDispName; // 服務顯示名稱
- if (ParamCount() > 1 && (isInstall || isUninstall)) // 指定了參數以及安裝、卸載標誌
- {
- // 先建立之
- Application->CreateForm(__classid(TMyService), &MyService);
- if ( isInstall || isUninstall ) // 對於安裝或卸載情況
- {
- for(int i = 1; i <= ParamCount(); i++) // 迴圈處理所有參數
- {
- AnsiString param = ParamStr(i).LowerCase(); // 為便於比較,先轉換成小寫
- int pos; // 參數位置
- if (param.Pos("/name:")) // 匹配服務名稱: "/name:xxxxx"
- {
- pos = param.Pos(":");
- serviceName = param.SubString(pos + 1, param.Length() - pos);
- if ((pos = serviceName.Pos(" ")) > 0) // 刪除空格,這個其實不必要,系統會指出名稱非法
- {
- serviceName.Delete(pos, 1);
- }
- }
- else
- if (param.Pos("/dispname:")) // 匹配服務顯示名稱: "/serviceDispName:xxxxx"
- {
- pos = param.Pos(":");
- serviceDispName = param.SubString(pos + 1, param.Length() - pos);
- }
- }
- if (serviceName.Length() > 0)
- Application->Components[0]->Name = serviceName; // 使用指定的服務名稱
- else
- serviceName = Application->Components[0]->Name; // 未指定服務名稱,會使用TMyService的預設名稱:MyService
- if (serviceDispName.Length() == 0)
- serviceDispName = serviceName; // 如果未指定顯示名稱,使用服務名稱來代替
-
- ((TService *)Application->Components[0])->DisplayName = serviceDispName; // 設定顯示名稱
- }
- }
- Application->Run(); //< 服務開始運行
現在,編譯並註冊我們的服務,可以看到它按指定的方式顯示在服務列表中,讓我們試著啟動它。。。。。。等等。。。。。啟動失敗!!!!OOOOOOOH! SHIT!!!!!
點解!?!?
再來查看SvrMgr.pas,BCB(或Delphi?)是這樣啟動服務滴:
- // .....
- begin
- Forms.Application.OnException := OnExceptionHandler; // 異常控制代碼
- ServiceCount := 0; // 服務物件(TXXXService)數量
- for i := 0 to ComponentCount - 1 do // Application包含的組件數量即服務物件數量
- if Components[i] is TService then Inc(ServiceCount);
- SetLength(ServiceStartTable, ServiceCount + 1); // 設定服務啟動表的尺寸
- FillChar(ServiceStartTable[0], SizeOf(TServiceTableEntry) * (ServiceCount + 1), 0); // 清零
- J := 0;
- for i := 0 to ComponentCount - 1 do // 填充服務入口表
- if Components[i] is TService then
- begin
- ServiceStartTable[J].lpServiceName := PChar(Components[i].Name); // 這裡使用的是Name屬性!
- ServiceStartTable[J].lpServiceProc := @ServiceMain; // 關於ServiceMain入口函數,請參閱Windows SDK help
- Inc(J);
- end;
- StartThread := TServiceStartThread.Create(ServiceStartTable); // 啟動服務
- // .....
- // TServiceStartThread的線程函數實現
- procedure TServiceStartThread.Execute;
- begin
- if StartServiceCtrlDispatcher(FServiceStartTable[0]) then // 使用服務入口表啟動服務
- ReturnValue := 0
- else
- ReturnValue := GetLastError;
- end;
Windows API StartServiceCtrlDispatcher()使用服務入口表啟動服務,該操作是名稱相關的,而我們已經使用了指定的名稱安裝服務,所以當系統(TServiceApplication)使用原有的名稱(這裡是MyService)來啟動服務時,會找不到名為MyService的服務(我們已經指定其名稱為ThisIsAnotherService),導致啟動失敗。由於系統在啟動服務時,沒有提供關於服務名稱的上下文,因此我們需要作一點手腳,創造這個上下文。簡單的方法是:在安裝服務時,修改服務的啟動路徑記錄,添加服務名稱作為參數。在服務啟動時,解析這個參數,並使用該參數修改TMyService->Name,這樣服務應能順利啟動。程式碼範例如下:
先添加額外的參數,儲存服務名稱:
- // .......
- Application->Run();
- // 需要在Run以後,此時服務已經成功安裝
- if (isInstall)
- {
- // 最簡單的方法是Hack註冊表
- TRegistry* reg = new TRegistry();
- AnsiString key = "//System//CurrentControlSet//Services//" + serviceName; // 註冊表路徑
- reg->RootKey = HKEY_LOCAL_MACHINE;
- if (reg->OpenKey(key, false)) // 開啟薦
- {
- AnsiString imagePath = reg->ReadString("ImagePath"); // 介就系程式映像的路徑
- reg->WriteString("ImagePath", imagePath + " -" + serviceName); // 我們要做的是添加額外的參數:服務名稱
- }
- reg->CloseKey();
- delete reg;
- } // if isInstall then hack registry
再添加對參數的處理:
- if (paramCount() > 1 && (isInstall || isUninstall))
- {
- //......
- }
- else // 非安裝,即啟動
- {
- // 檢查參數個數
- if (ParamCount() > 0)
- {
- AnsiString extraParam = ParamStr(1).LowerCase(); // 額外參數,轉換為小寫
- AnsiString specifiedServiceName;
- if (extraParam.Pos("-")) // 定位"-"
- {
- int pos = extraParam.Pos("-");
- specifiedServiceName = extraParam.SubString(pos + 1, extraParam.Length() - pos); // 解析服務名稱
- }
- if (!specifiedServiceName.Length()) // 如果名稱無效,則使用預設名稱:MyService
- {
- Application->CreateForm(__classid(TMyService), &MyService); // 使用預設名稱
- }
- else
- {
- Application->CreateForm(__classid(TMyService), &MyService);
- MyService->Name = specifiedServiceName; // 使用指定的名稱
- }
- } //
- } // if ... else
- // ...
- Application->Run();
- // ...
現在,編譯器,重新安裝服務,再試著啟動一下。。。。。。
如果沒有錯誤的話,服務應該能夠順利啟動。(廢話)
BCB沒有提供服務的描述屬性,這也可以通過修改註冊表的方法實現,操作很簡單,這裡不在多言。
此乃末技。。。。。