unit u_processes;

{$I u_defines.inc}

interface

uses
  Classes, SysUtils, ExtCtrls, process, asyncprocess, pipes, u_common;

type

  {
    The standard process wrapper that used accross the applicaton.

    This class solves several issues encountered when using TProcess and TAsyncProcess:

    -   "OnTerminate" event is never called under Linux.
        FIX: a timer perdiodically check the process and call the event accordingly.
    -   "OnReadData" event is not usable to read full output lines.
        FIX: the output is accumulated in a TMemoryStream which allows to keep data
        at the left of an unterminated line when a buffer is available.
    -   When StdErr is redirect to "Output", both streams can be blended.
        FIX: the option is deactivated on execution, a flag is set, and the error
        stream is always appended after the output.

    The member "Output" is not usable anymore. Instead:

    -   "getFullLines()" can be used in "OnReadData" or after the execution to fill
        a string list.
    -   "StdoutEx" can be used to read the raw output. It allows to seek, which
        overcomes another limitation of the basic process classes.
  }
  TDexedProcess = class(TASyncProcess)
  private
    class var FAutoKillProcThreshold: dword;
  var
    fErrToOut: boolean;
    fRealOnTerminate: TNotifyEvent;
    fRealOnReadData: TNotifyEvent;
    fStdoutEx: TMemoryStream;
    fStderrEx: TMemoryStream;
    fTerminateChecker: TTimer;
    fDoneTerminated: boolean;
    fHasRead: boolean;
    fAutoKilled: boolean;
    procedure checkTerminated(sender: TObject);
    procedure setOnTerminate(value: TNotifyEvent);
    procedure setOnReadData(value: TNotifyEvent);
  protected
    procedure internalDoOnReadData(sender: TObject); virtual;
    procedure internalDoOnTerminate(sender: TObject); virtual;
  published
    property OnTerminate write setOnTerminate;
    property OnReadData write setOnReadData;
  public
    constructor create(aOwner: TComponent); override;
    destructor destroy; override;
    procedure execute; override;
    // reads TProcess.Output and StdErr in their "Ex" versions.
    procedure fillOutputStack;
    // fills list with the full lines contained in StdoutEx
    procedure getFullLines(list: TStrings; consume: boolean = true);
    // access to a flexible copy of TProcess.Output
    property StdoutEx: TMemoryStream read fStdoutEx;
    // access to a flexible copy of TProcess.Error
    property StdErrEx: TMemoryStream read fStderrEx;
    // indicates if an output buffer is read
    property hasRead: boolean read fHasRead;
    // indicates if OnTerminated was called
    property doneTerminated: boolean read fDoneTerminated;
    // indicates of the process was autokilled
    property autoKilled: boolean read fAutoKilled;
    // auto kill the process if its output reach this size
    class property autoKillProcThreshold: dword read FAutoKillProcThreshold write FAutoKillProcThreshold;
  end;

  {
    OnReadData is only called if no additional buffers are passed
    during a timeout.
  }
  TAutoBufferedProcess = class(TDexedProcess)
  private
    fNewBufferChecker: TTimer;
    fNewBufferTimeOut: Integer;
    fPreviousSize: Integer;
    procedure newBufferCheckerChecks(sender: TObject);
    procedure setTimeout(value: integer);
  protected
    procedure internalDoOnReadData(sender: TObject); override;
    procedure internalDoOnTerminate(sender: TObject); override;
  public
    constructor create(aOwner: TComponent); override;
    procedure execute; override;
    property timeOut: integer read fNewBufferTimeOut write setTimeout;
  end;

  procedure killProcess(var proc: TDexedProcess);

  function prettyReturnStatus(proc: TProcess): string;

implementation

procedure killProcess(var proc: TDexedProcess);
begin
  if proc.isNotAssigned then
    exit;
  if proc.Running then
    proc.Terminate(0);
  proc.Free;
  proc := nil;
end;

function prettyReturnStatus(proc: TProcess): string;
var
  s: integer;
  {$IFDEF UNIX}
  u: integer;
  {$ENDIF}
begin
  result := '';
  s := proc.ExitStatus;
  {$IFDEF UNIX}
  if s > 255 then
  begin
    u := s div 256;
    result := intToStr(u) + ' (Program-defined exit status)';
  end
  else if s > 127 then
  begin
    u := s - 128;
    if s > 128 then
    case u of
      0: result := '128 (Invalid argument to exit)';
      1: result := '1 (SIGHUP)';
      2: result := '2 (SIGINT)';
      3: result := '3 (SIGQUIT)';
      4: result := '4 (SIGILL)';
      5: result := '4 (SIGTRAP)';
      6: result := '6 (SIGABRT)';
      7: result := '7 (SIGEMT)';
      8: result := '8 (SIGFPE)';
      9: result := '9 (SIGKILL)';
      10: result := '10 (SIGBUS)';
      11: result := '11 (SIGSEGV)';
      12: result := '12 (SIGSYS)';
      13: result := '13 (SIGPIPE)';
      14: result := '14 (SIGALRM)';
      15: result := '15 (SIGTERM)';
      16: result := '16 (SIGUSR1)';
      17: result := '17 (SIGUSR2)';
      18: result := '18 (SIGCHLD)';
      19: result := '19 (SIGPWR)';
      20: result := '20 (SIGWINCH)';
      21: result := '21 (SIGURG)';
      22: result := '22 (SIGPOLL)';
      23: result := '23 (SIGSTOP)';
      24: result := '24 (SIGTSTP)';
      25: result := '25 (SIGCONT)';
      26: result := '26 (SIGTTIN)';
      27: result := '27 (SIGTTOU)';
      28: result := '28 (SIGVTALRM)';
      29: result := '29 (SIGPROF)';
      30: result := '30 (SIGXCPU)';
      31: result := '31 (SIGXFSZ)';
      32: result := '32 (SIGWAITING)';
      33: result := '33 (SIGLWP)';
      34: result := '34 (SIGAIO)';
    end;
  end;
  {$ENDIF}
  if result = '' then
    result := intToStr(s) + ' (undeterminated meaning)';
end;

constructor TDexedProcess.create(aOwner: TComponent);
begin
  inherited;
  fStdoutEx := TMemoryStream.Create;
  fStderrEx := TMemoryStream.Create;
  FTerminateChecker := TTimer.Create(nil);
  FTerminateChecker.Interval := 200;
  fTerminateChecker.OnTimer := @checkTerminated;
  fTerminateChecker.Enabled := false;
  TAsyncProcess(self).OnTerminate := @internalDoOnTerminate;
  TAsyncProcess(self).OnReadData := @internalDoOnReadData;
end;

destructor TDexedProcess.destroy;
begin
  FTerminateChecker.Free;
  fStdoutEx.Free;
  fStderrEx.Free;
  inherited;
end;

procedure TDexedProcess.Execute;
begin
  fAutoKilled := false;
  fHasRead := false;
  fStdoutEx.Clear;
  fStderrEx.Clear;
  fDoneTerminated := false;
  fErrToOut := poStderrToOutPut in Options;
  if fErrToOut then
    Options := Options - [poStderrToOutPut];
  TAsyncProcess(self).OnReadData := @internalDoOnReadData;
  TAsyncProcess(self).OnTerminate := @internalDoOnTerminate;
  fTerminateChecker.Enabled := true;
  inherited;
end;

procedure TDexedProcess.fillOutputStack;

  procedure fill(inStr: TInputPipeStream; outStr: TMemoryStream);
  var
    s: integer;
    c: integer;
  begin
    s := outStr.size;
    while inStr.isAssigned and (inStr.NumBytesAvailable > 0) do
    begin
      outStr.SetSize(s + 1024);
      c := inStr.Read((outStr.Memory + s)^, 1024);
      s += c;

      if (FAutoKillProcThreshold <> 0) and not fDoneTerminated and
        (fStderrEx.Size + fStdoutEx.Size >= FAutoKillProcThreshold) then
      begin
        fStdoutEx.Clear;
        fAutoKilled := true;
        Terminate(1);
        exit;
      end;

    end;
    outStr.SetSize(s);
  end;

begin
  if not (poUsePipes in Options) then
    exit;

  if Output.isAssigned then
    fill(Output, StdoutEx);
  if Stderr.isAssigned then
    fill(Stderr, stderrEx);
end;

procedure TDexedProcess.getFullLines(list: TStrings; consume: boolean = true);
var
  stored: Integer;
  lastTerm: Integer;
  toread: Integer;
  buff: Byte = 0;
  str: TMemoryStream;
begin
  stored := fStdoutEx.Position;
  if not Running then
  begin
    // stderr has been read in its own stream
    // preventing interleaving, now put it at the end...
    if (poStderrToOutPut in Options) or fErrToOut then
    begin
      fStdoutEx.Position:=fStdoutEx.Size;
      fStdoutEx.Write(fStderrEx.Memory^, fStderrEx.Size);
      if consume then
        fStderrEx.Clear;
    end;
    fStdoutEx.Position:=stored;
    list.LoadFromStream(fStdoutEx);
    if consume then
      fStdoutEx.Clear;
  end else
  begin
    lastTerm := fStdoutEx.Position;
    while fStdoutEx.Read(buff, 1) = 1 do
      if buff = 10 then
        lastTerm := fStdoutEx.Position;
    fStdoutEx.Position := stored;
    if lastTerm <> stored then
    begin
      str := TMemoryStream.Create;
      try
        toread := lastTerm - stored;
        str.SetSize(toRead);
        fStdoutEx.Read(str.Memory^, toread);
        list.LoadFromStream(str);
      finally
        str.Free;
      end;
    end;
  end;
end;

procedure TDexedProcess.setOnTerminate(value: TNotifyEvent);
begin
  fRealOnTerminate := value;

  //TAsyncProcess(self).OnTerminate := @internalDoOnTerminate;
end;

procedure TDexedProcess.setOnReadData(value: TNotifyEvent);
begin
  fRealOnReadData := value;

  //TAsyncProcess(self).OnReadData := @internalDoOnReadData;
end;

procedure TDexedProcess.internalDoOnReadData(sender: TObject);
begin
  fHasRead := true;
  fillOutputStack;
  if fRealOnReadData <> nil then
    fRealOnReadData(self);
end;

procedure TDexedProcess.internalDoOnTerminate(sender: TObject);
begin
  fHasRead := false;
  fTerminateChecker.Enabled := false;
  if fDoneTerminated then
    exit;
  fDoneTerminated := true;

  // restore if same proc is called again,
  // self.execute will exclude the option.
  if fErrToOut then
    Options := Options + [poStderrToOutPut];

  // note: made to fix a leak in the process used by the linter
  // onTerminate is sometimes determined by an internal timer
  // and not the base method of TAsyncProcess (which usually unhooks)
  //UnhookPipeHandle;
  //UnhookProcessHandle;

  fillOutputStack;
  if fRealOnTerminate <> nil then
    fRealOnTerminate(self);
end;

procedure TDexedProcess.checkTerminated(sender: TObject);
begin
  if Running then
  begin
    if Output.isAssigned and StdErr.isAssigned then
      if Output.NumBytesAvailable + Stderr.NumBytesAvailable > 0 then
        internalDoOnReadData(self);
    exit;
  end;
  fTerminateChecker.Enabled := false;
  internalDoOnTerminate(self);
end;

constructor TAutoBufferedProcess.create(aOwner: TComponent);
begin
  inherited;
  fNewBufferTimeOut := 1000;
  fNewBufferChecker := TTimer.Create(self);
  fNewBufferChecker.Enabled:= false;
  fNewBufferChecker.Interval:= fNewBufferTimeOut;
  fNewBufferChecker.OnTimer:= @newBufferCheckerChecks;
end;

procedure TAutoBufferedProcess.setTimeout(value: integer);
begin
  if fNewBufferTimeOut = value then
    exit;
  fNewBufferTimeOut := value;
  fNewBufferChecker.Interval:= fNewBufferTimeOut;
end;

procedure TAutoBufferedProcess.execute;
begin
  fPreviousSize := fStdoutEx.Size;
  fNewBufferChecker.Enabled:=true;
  inherited;
end;

procedure TAutoBufferedProcess.newBufferCheckerChecks(sender: TObject);
begin
  if fStdoutEx.Size = fPreviousSize then
  begin
    if assigned(fRealOnReadData) then
      fRealOnReadData(self);
  end;
  fPreviousSize := fStdoutEx.Size;
end;

procedure TAutoBufferedProcess.internalDoOnReadData(sender: TObject);
begin
  fillOutputStack;
end;

procedure TAutoBufferedProcess.internalDoOnTerminate(sender: TObject);
begin
  fNewBufferChecker.Enabled:=false;
  inherited;
end;

end.

