mirror of https://gitlab.com/basile.b/dexed.git
402 lines
11 KiB
Plaintext
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.
|
|
|