unit ProcessCheckerClass;
{*******************************************************************************
  ProcessChecker Demo
  Written by David Clegg, davidclegg@optusnet.com.au.

  This unit contains the TProcessChecker class, which is responsible for
  monitoring, stopping and restarting processes.
*******************************************************************************}
interface

uses
  ProcessCheckerSettings, System.Windows.Forms, System.Diagnostics,
  ProcessClasses;

type
  TProcessCheckerEvent = procedure (Sender: TObject; const EventMessage: string) of object;
  TProcessArray = Array of Process;

	/// <summary>
	/// Class to handle the Process monitoring.
	/// </summary>
  TProcessChecker = class
  strict private
    FOnEvent: TProcessCheckerEvent;
    FSettings: TProcessCheckerSettings;
    FSuspended: boolean;
    FTimer: Timer;
    procedure InitTimer;
    procedure InitSettings;
    procedure SetTimerInterval;
    procedure CheckProcesses;
    procedure TimerTick(Sender: TObject; Args: EventArgs);
    procedure SettingsUpdated(Sender: TObject; Args: EventArgs);
    function CheckProcess(pProcessPath: string): TProcessArray;
    procedure RestartProcess(pProcess: TWatchedProcess);
    procedure NotifyEvent(const pEventDetails: string);
    procedure StopProcess(pProcess: Process);
    procedure StopDependantProcesses(pProcess: TWatchedProcess);
  public
    property OnEvent: TProcessCheckerEvent read FOnEvent write FOnEvent;
    property Settings: TProcessCheckerSettings read FSettings write FSettings;
    property Suspended: boolean read FSuspended write FSuspended;
    constructor Create(const pSettings: TProcessCheckerSettings);
  end;

implementation

uses
  System.IO, System.Threading, SysUtils;

constructor TProcessChecker.Create(const pSettings: TProcessCheckerSettings);
begin
  inherited Create;
  FSettings := pSettings;
  InitTimer;
  InitSettings;
end;

/// <summary>
/// Create the timer, and attach the Tick event.
/// </summary>
procedure TProcessChecker.InitTimer;
begin
  FTimer := System.Windows.Forms.Timer.Create;
  Include(FTimer.Tick, TimerTick);
  SetTimerInterval;
end;

/// <summary>
/// Sets the interval for the timer used to trigger the process checking
/// </summary>
procedure TProcessChecker.SetTimerInterval;
var
  lInterval: integer;
begin
  //CheckFrequency is stored in seconds, but we need to convert it
  //to milliseconds for the timer.
  lInterval := FSettings.CheckFrequency * 1000;

  //The Timer class will throw an exception if we attempt to assign an
  //interval with a zero value
  if (lInterval > 0) then
  begin
    FTimer.Interval := lInterval;
    FTimer.Enabled := FSettings.Enabled;
  end
  else
  begin
    //Ensure timer is disabled
    FTimer.Enabled := false;
    if FSettings.Enabled then
    begin
      FSettings.Enabled := false;
      FSettings.Save;
    end;
  end;
end;

procedure TProcessChecker.TimerTick(Sender: TObject; Args: EventArgs);
begin
  CheckProcesses;
end;

/// <summary>
/// Create the ProcessCheckerSettings instance, and attach the event
/// handlers to notify when the instance is loaded or saved.
/// </summary>
procedure TProcessChecker.InitSettings;
begin
  Include(FSettings.SettingsLoaded, SettingsUpdated);
  Include(FSettings.SettingsSaved, SettingsUpdated);

  //As the event handlers are attached after the settings are
  //initially loaded from disk, we need to fire the event manually.
  SettingsUpdated(Self, nil);
end;

/// <summary>
/// Event handler called if the ProcessCheckerSettings instance is loaded
/// or saved. As we are only interested in determining that the settings
/// may have changed (and not what caused the change), we can share the
/// event handler.
/// </summary)
procedure TProcessChecker.SettingsUpdated(Sender: TObject; Args: EventArgs);
begin
  SetTimerInterval;
end;

/// <summary>
/// Iterate through all monitored processes to check whether they are
/// still running.
/// </summary>
procedure TProcessChecker.CheckProcesses;
var
  lProcesses: TProcessArray;
  i: integer;
  j: integer;
  lWatchedProcess: TWatchedProcess;
  lRunningProcess: Process;
begin
  if not FSuspended then
    for i := 0 to FSettings.WatchedProcesses.Count -1 do
    begin
      lWatchedProcess := FSettings.WatchedProcesses[i] as TWatchedProcess;
      lProcesses := CheckProcess(lWatchedProcess.Path);
      if (Length(lProcesses) = 0) then
      begin
        //No instances of lWatchedProcess running, so re-start it
        RestartProcess(lWatchedProcess);
        if (FSettings.RestartOneProcess) then
          //We only restart one process each time we check
          //running processes. Any subsequent processes that have
          //unexpectedly terminated will be caught next time.
          break;
      end
      else
        for j := 0 to Length(lProcesses) -1 do
        begin
          //At least one instance of lWatchedProcess running,
          //so check that the instance(s) are still responding.
          lRunningProcess := lProcesses[j];
          if lWatchedProcess.CheckResponding then
            if not (lRunningProcess.Responding) then
            begin
              //Process not responding, so re-start it.
              NotifyEvent(Format('%s - not responding',
                [lRunningProcess.ProcessName]));
              StopProcess(lRunningProcess);
              RestartProcess(lWatchedProcess);
              if (FSettings.RestartOneProcess) then
                //Only restarting one process per check.
                break;
            end;
        end;
  end;
end;

/// <summary>
/// Check to see whether the specified process is still running.
/// Returns an array of Process instances if the process has at least one
/// running instance.
/// </summary>
function TProcessChecker.CheckProcess(pProcessPath: string): TProcessArray;
var
  lProcessName: string;
begin
  lProcessName := System.IO.Path.GetFileNameWithoutExtension(pProcessPath);
  Result := Process.GetProcessesByName(lProcessName);
end;

/// <summary>
/// Restart a process.
/// </summary>
procedure TProcessChecker.RestartProcess(pProcess: TWatchedProcess);
var
  lStartProcess: Process;
begin
  StopDependantProcesses(pProcess);
  lStartProcess := Process.Create;
  lStartProcess.StartInfo.FileName := pProcess.Path;
  lStartProcess.StartInfo.WorkingDirectory := Path.GetDirectoryName(pProcess.Path);
  try
    lStartProcess.Start();
    NotifyEvent(Format('%s - Process restarted', [pProcess.Name]));
  except on E: Exception do
    NotifyEvent(Format('Error starting %s - %s',
      [pProcess.Name, E.Message]));
  end;
end;

/// <summary>
/// Iterate through, and terminate, all dependant processes.
/// </summary>
procedure TProcessChecker.StopDependantProcesses(pProcess: TWatchedProcess);
var
  lDependantProcesses: TProcessArray;
  i: integer;
  j: integer;
begin
  for i := 0 to pProcess.DependantProcesses.Count -1 do
  begin
    lDependantProcesses := CheckProcess(pProcess.DependantProcesses[i].Path);
    for j := 0 to Length(lDependantProcesses) -1 do
      StopProcess(lDependantProcesses[j]);
  end;
end;

/// <summary>
/// Stop a process.
/// </summary>
procedure TProcessChecker.StopProcess(pProcess: Process);
begin
  //Attempt to stop the process 'nicely' by sending a Close message
  //to the processes main window
  pProcess.CloseMainWindow();
  //Give the message a chance to be processed
  Thread.Sleep(100);
  if not pProcess.HasExited then
  begin
    //CloseMainWindow didn't work so attempt a more forceful
    //shutdown. Process.Kill forces an immediate termination of the
    //process.
    pProcess.Kill;
    Thread.Sleep(100);
    if not pProcess.HasExited then
      //Still running, but its time to admit defeat :-(
      NotifyEvent(Format('%s - Process termination failed',
        [pProcess.ProcessName]))
    else
      NotifyEvent(Format('%s - Process killed', [pProcess.ProcessName]));
  end
  else
    NotifyEvent(Format('%s - Process stopped' , [pProcess.ProcessName]));
end;

/// <summary>
/// Send a Process Checker Event notification.
/// </summary>
procedure TProcessChecker.NotifyEvent(const pEventDetails: string);
begin
  if Assigned(FOnEvent) then
    FOnEvent(Self, pEventDetails);
end;

end.



