Restarting Services (and maybe other exe's)

Comment on this article

Assuming 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:

  1. Attempt to stop the service through the Service Control Manager (SCM)
  2. Terminate the executable the service is running in
  3. Start the service anew through SCM

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:

  • Sufficient rights to terminate processes
  • The process ID of the process to terminate

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:

  1. Open the process token
  2. Locate the privilege value in that token
  3. Adjust the privilege value in the token
  4. Close the process token

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.

Comment on this article

TOP