dexed/src/u_processes.pas

402 lines
11 KiB
Plaintext

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.