Home Forum RSS PGP Alerts Links (D) |
|
Restarting Services (and maybe other exe's) Comment on this articleAssuming you already have a way of finding out when a service of yours has hung or consumed too many resources, you still need a way to stop and restart it. Having the service exit itself often does not work, so you have to have another service that does it for you. That other service, once it decides to restart your service, should:
Note that I always terminate the executable even if step 1 succeeded in stopping the service. That's because a limping service often does say it stopped alright, even though the process itself does not exit. This happens, for instance, if a child thread does not want to exit. All the code here is in Delphi 6, but it's pretty much the same in most other languages, I assume. An important security caveat If you use this system, or part of it, to restart non-service executables, take care not to fall into an "elevation of privileges" trap. Imagine an executable that is started by a user with limited rights and which makes your application restart it. Then the executable will be run with the privileges of the starting application. If that application is a service running as local system, for instance, the executable may gain practically unlimited rights. So, if you don't know exactly what you're doing, limit yourself to restarting services, which automatically run under the user configured for the service, not the user asking for the service to be started. Stopping using the SCM In what follows I assume you have both the service name and the full path to the module. For instance, the service name can be "MySvc", while the full path to the module can be "c:\projects\MySvcThing\MySvcProggie.exe". How you get these is outside of the scope of this article, but a natural way would be for the service you're keeping an eye on to regularly dump this information into a file, a named pipe or similar. Once the monitored service does not report within a certain time interval, the restart mechanism swings into action. To interact with the SCM, I build a class that connects to the SCM, opens the handle for a given service and then gives you methods to interrogate status, start and stop the service, among other things. In my usual fashion, I only hand out interface pointers to the class through a creator function (makeSvcCtrl) while hiding the real class constructor in the implementation section. It's a habit I have and it saves me oodles of time looking for object lifetime problems. You have to take care that the process using this class does have sufficient rights to start and stop services and to kill processes. If this class is built into a service that runs as local system, then this should be no problem. In the class and interface, you'll find helper functions that tells you the state the service is in and helps you to change that state. Terminating a process In order to terminate a process, you first need to have:
If you run as local system, you will probably have sufficient rights. You need to have the "debug" privilege, which allows you to hook into other processes. But even if you have that privilege, it isn't enabled, so you have to enable it first. To enable or disable a privilege, you have to:
You can see all this in action in the SetPrivilege() function below. The next step is to locate the process ID for the process you want to terminate. You may get the idea that you'll have the monitored process itself give you this ID as it starts up or every regular time interval, but that is a very bad idea. Imagine that the process sends your monitoring service a message every 30 seconds giving it the process ID it has. And that your monitoring service kills the process using that ID if it didn't get such a message for more than 30 seconds. What happens if the monitored service exits unexpectedly and another process starts and gets that same ID? You guessed it... you're monitoring service will now kill the wrong process. If we instead use the module name and look up the ID every time we need it, we simply won't find it if the process isn't running anymore. Seems a lot safer to me. In order to find the process ID of the process, it's easiest to use the toolhelp functions provided by the tlhelp32.dll and wrapped by the Delphi unit TlHelp32.pas. The KillProcessByName() function uses a toolhelp "snapshot" of running processes which it then searches for a process with the given module name. If it finds it, it then calls KillProcessByPID() with the process ID that it found. KillProcessByPID() enables the debug privilege, opens a handle to the process and then calls TerminateProcess() on it. After that, it disables the debug privilege again (it's good security practice not to enable privileges longer than absolutely necessary). The unit in all its glory Below is the full source for the unit that allows you to start/stop and murder services. Use it wisely. {-------------------------------------------------------------------- Author: J.M.Wehlou, 2003 Description: Handles process start/stop functions, including services --------------------------------------------------------------------} unit ProcessFuncs; interface type eSvcStatus = (essUnknown, essRunning, essPaused, essStopped, essStarting, essStopping, essPausePending, essContinuePending); eSvcCmd = (escStart, escStop, escPause, escContinue); ISvcCtrl = interface function GetSvcStatus(): eSvcStatus; procedure SvcCmd(cmd: eSvcCmd); function IsConnected(): boolean; function StopOrKillService(const iTimeOutMs: cardinal): boolean; end; function KillProcessByName(const sName: string): boolean; function makeSvcCtrl(const sSvcName: string; const sModName: string): ISvcCtrl; // ================================================================== implementation uses SysUtils, DateUtils, Windows, WinTypes, WinSvc, TlHelp32; type TSvcCtrl = class (TInterfacedObject, ISvcCtrl) fsSvcName : string; fsModName : string; fhSCM : THandle; fhSvc : THandle; constructor Create(const sSvcName: string; const sModName: string); destructor Destroy; override; function IsConnected: boolean; function GetSvcStatus(): eSvcStatus; procedure SvcCmd(cmd: eSvcCmd); function StopOrKillService(const iTimeOutMs: cardinal): boolean; function StopService(const iTimeOutMs: cardinal): boolean; end; // ================================================================== function makeSvcCtrl(const sSvcName: string; const sModName: string): ISvcCtrl; begin Result := TSvcCtrl.Create(sSvcName, sModName); end; // ================================================================== function SetPrivilege(aPrivilegeName: string; aEnabled: boolean): boolean; var TP : TTokenPrivileges; TPPrev : TTokenPrivileges; Token : THandle; dwRetLen : DWord; begin Result := False; OpenProcessToken(GetCurrentProcess, TOKEN_ADJUST_PRIVILEGES or TOKEN_QUERY, Token); TP.PrivilegeCount := 1; if (LookupPrivilegeValue(nil, PChar(aPrivilegeName), TP.Privileges[0].LUID)) then begin if (aEnabled) then TP.Privileges[0].Attributes := SE_PRIVILEGE_ENABLED else TP.Privileges[0].Attributes := 0; dwRetLen := 0; Result := WinTypes.AdjustTokenPrivileges(Token, False, TP, SizeOf(TPPrev), TPPrev, dwRetLen); end; CloseHandle(Token); end; // ------------------------------------------------------------------ function KillProcessByPID(pid: cardinal): boolean; var hProc : THandle; begin Result := False; // you need debug privilege to kill a service if not SetPrivilege('SeDebugPrivilege', True) then exit; hProc := OpenProcess(STANDARD_RIGHTS_REQUIRED or PROCESS_TERMINATE, False, pid); try if hProc > 0 then begin Result := TerminateProcess(hProc, 1); end; finally CloseHandle(hProc); SetPrivilege('SeDebugPrivilege', False); end; end; // ------------------------------------------------------------------ function KillProcessByName(const sName: string): boolean; var snap : THandle; ppe : PROCESSENTRY32; bRes : boolean; sNoPath : string; begin Result := False; ZeroMemory(@ppe, sizeof(ppe)); ppe.dwSize := sizeof(ppe); sNoPath := ExtractFileName(sName); // you also find services using their bare exe name snap := CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); try bRes := Process32First(snap, ppe); while (bRes) do begin if ppe.szExeFile = sNoPath then begin Result := KillProcessByPID(ppe.th32ProcessID); exit; end; ppe.dwSize := sizeof(ppe); bRes := Process32Next(snap, ppe); end; finally CloseHandle(snap); end; end; // ================================================================== { TSvcCtrl } constructor TSvcCtrl.Create(const sSvcName: string; const sModName: string); begin fsSvcName := sSvcName; fsModName := sModName; fhSCM := OpenSCManager(nil, nil, SC_MANAGER_ALL_ACCESS); fhSvc := 0; if (fhSCM <> 0) then begin fhSvc := OpenService(fhSCM, PChar(fsSvcName), SERVICE_ALL_ACCESS); end; end; // ------------------------------------------------------------------ destructor TSvcCtrl.Destroy; begin if (fhSvc <> 0) then CloseServiceHandle(fhSvc); if (fhSCM <> 0) then CloseServiceHandle(fhSCM); inherited; end; // ------------------------------------------------------------------ function TSvcCtrl.GetSvcStatus: eSvcStatus; var st : _SERVICE_STATUS; begin Result := essUnknown; if IsConnected() and QueryServiceStatus(fhSvc, st) then begin case st.dwCurrentState of SERVICE_STOPPED: Result := essStopped; SERVICE_START_PENDING: Result := essStarting; SERVICE_STOP_PENDING: Result := essStopping; SERVICE_RUNNING: Result := essRunning; SERVICE_CONTINUE_PENDING: Result := essContinuePending; SERVICE_PAUSE_PENDING: Result := essPausePending; SERVICE_PAUSED: Result := essPaused; end; // case end; end; // ------------------------------------------------------------------ function TSvcCtrl.IsConnected: boolean; begin Result := (fhSCM <> 0) and (fhSvc <> 0); end; // ------------------------------------------------------------------ // tries to stop the service the normal way and if it doesn't succeed // within the timeout, goes on to kill it via terminateprocedure function TSvcCtrl.StopOrKillService(const iTimeOutMs: cardinal): boolean; begin // trivial case first if GetSvcStatus() = essStopped then Result := True else if StopService(iTimeOutMs) then Result := True else Result := KillProcessByName(fsModName); end; // ------------------------------------------------------------------ function TSvcCtrl.StopService(const iTimeOutMs: cardinal): boolean; var startTime : TDateTime; begin startTime := Now; Result := False; SvcCmd(escStop); repeat SleepEx(100, False); Result := (GetSvcStatus() = essStopped); until (MilliSecondsBetween(Now, startTime) > iTimeOutMs) or (Result = True); end; // ------------------------------------------------------------------ procedure TSvcCtrl.SvcCmd(cmd: eSvcCmd); var stat : eSvcStatus; procedure SendSvcCmd(cmd: eSvcCmd); var dummy : PChar; SvcStatus : SERVICE_STATUS; begin case cmd of escStart : StartService(fhSvc, 0, dummy); escStop : ControlService(fhSvc, SERVICE_CONTROL_STOP, SvcStatus); escPause : ControlService(fhSvc, SERVICE_CONTROL_PAUSE, SvcStatus); escContinue : ControlService(fhSvc, SERVICE_CONTROL_CONTINUE, SvcStatus); end; end; begin stat := GetSvcStatus(); case cmd of escStart : if stat in [essStopped] then SendSvcCmd(cmd); escStop : if stat in [essRunning, essPaused, essPausePending, essContinuePending] then SendSvcCmd(cmd); escPause : if stat in [essRunning] then SendSvcCmd(cmd); escContinue : if stat in [essPaused] then SendSvcCmd(cmd); end; end; // ------------------------------------------------------------------ end. |
TOP |