dexed/src/ce_processes.pas

394 lines
10 KiB
Plaintext

unit ce_processes;
{$I ce_defines.inc}
interface
uses
Classes, SysUtils, ExtCtrls, process, asyncprocess;
type
//TODO: follow up https://bugs.freepascal.org/view.php?id=33897
//which is the cause of the timer workaround
{
The stanndard process wrapper used in Coedit.
This class solves several issues encountered when using TProcess and TAsyncProcess:
- OnTerminate event is never called under Linux.
Here a timer perdiodically check the process and call the event accordingly.
(STILL DOES NOT WORK)
- TAsyncProcess.OnReadData event is not usable to read full output lines.
Here the output is accumulated in a TMemoryStream which allows to keep data
at the left of an unterminated line when a buffer is available.
The member Output is not usable anymore. Instead:
- getFullLines() can be used in OnReadData or after the execution to fill
a string list.
- OutputStack can be used to read the raw output. It allows to seek, which
overcomes another limitation of the basic process classes.
}
TCEProcess = class(TASyncProcess)
private
fRealOnTerminate: TNotifyEvent;
fRealOnReadData: TNotifyEvent;
fOutputStack: TMemoryStream;
fStdError: TMemoryStream;
fTerminateChecker: TTimer;
fDoneTerminated: boolean;
fHasRead: boolean;
fRedirectStdErr: 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;
// Check if process is terminated (bug 33897).
// Not to be called in the OnTerminated handler.
procedure blockingWait;
// Add stderr to stdout, to be called when terminated
procedure appendStdErr;
// reads TProcess.Output in OutputStack
procedure fillOutputStack;
// fills list with the full lines contained in OutputStack
procedure getFullLines(list: TStrings; consume: boolean = true);
// access to a flexible copy of TProcess.Output
property OutputStack: TMemoryStream read fOutputStack;
// indicates if an output buffer is read
property hasRead: boolean read fHasRead;
end;
{
OnReadData is only called if no additional buffers are passed
during a timeout.
}
TCEAutoBufferedProcess = class(TCEProcess)
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: TCEProcess);
function prettyReturnStatus(proc: TProcess): string;
implementation
procedure killProcess(var proc: TCEProcess);
begin
if proc = nil 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 TCEProcess.create(aOwner: TComponent);
begin
inherited;
FOutputStack := TMemoryStream.Create;
fStdError := TMemoryStream.Create;
FTerminateChecker := TTimer.Create(nil);
FTerminateChecker.Interval := 100;
fTerminateChecker.OnTimer := @checkTerminated;
fTerminateChecker.Enabled := false;
TAsyncProcess(self).OnTerminate := @internalDoOnTerminate;
TAsyncProcess(self).OnReadData := @internalDoOnReadData;
end;
destructor TCEProcess.destroy;
begin
FTerminateChecker.Free;
FOutputStack.Free;
fStdError.Free;
inherited;
end;
procedure TCEProcess.Execute;
begin
fRedirectStdErr := poStderrToOutPut in Options;
Options := Options - [poStderrToOutPut];
fHasRead := false;
fOutputStack.Clear;
fStdError.Clear;
fDoneTerminated := false;
TAsyncProcess(self).OnReadData := @internalDoOnReadData;
TAsyncProcess(self).OnTerminate := @internalDoOnTerminate;
fTerminateChecker.Enabled := true;
inherited;
end;
procedure TCEProcess.fillOutputStack;
var
sum, cnt: Integer;
begin
if not (poUsePipes in Options) then
exit;
// output
sum := fOutputStack.Size;
while (Output <> nil) and (NumBytesAvailable > 0) do
begin
fOutputStack.SetSize(sum + NumBytesAvailable);
cnt := Output.Read((fOutputStack.Memory + sum)^, NumBytesAvailable);
sum += cnt;
end;
fOutputStack.SetSize(sum);
if not fRedirectStdErr then
exit;
// error
sum := fStdError.Size;
while (Stderr <> nil) and (Stderr.NumBytesAvailable > 0) do
begin
fStdError.SetSize(sum + Stderr.NumBytesAvailable);
cnt := Stderr.Read((fStdError.Memory + sum)^, Stderr.NumBytesAvailable);
sum += cnt;
end;
fStdError.SetSize(sum);
end;
procedure TCEProcess.appendStdErr;
var
sum, cnt: Integer;
begin
if not (poUsePipes in Options) or not fRedirectStdErr then
exit;
fillOutputStack;
fStdError.Position:=0;
sum := fOutputStack.Size;
fOutputStack.SetSize(sum + fStdError.size);
fStdError.Read((fOutputStack.Memory + sum)^, fStdError.Size);
end;
procedure TCEProcess.getFullLines(list: TStrings; consume: boolean = true);
var
stored: Integer;
lastEOL: Integer;
toread: Integer;
buff: Byte = 0;
str: TMemoryStream;
begin
if not Running then
begin
list.LoadFromStream(fOutputStack);
if consume then
fOutputStack.Clear;
end else
begin
lastEOL := fOutputStack.Position;
stored := fOutputStack.Position;
while fOutputStack.Read(buff, 1) = 1 do
if buff = 10 then
lastEOL := fOutputStack.Position;
fOutputStack.Position := stored;
if lastEOL <> stored then
begin
str := TMemoryStream.Create;
try
toread := lastEOL - stored;
str.SetSize(toRead);
fOutputStack.Read(str.Memory^, toread);
list.LoadFromStream(str);
finally
str.Free;
end;
end;
end;
end;
procedure TCEProcess.setOnTerminate(value: TNotifyEvent);
begin
fRealOnTerminate := value;
TAsyncProcess(self).OnTerminate := @internalDoOnTerminate;
end;
procedure TCEProcess.setOnReadData(value: TNotifyEvent);
begin
fRealOnReadData := value;
TAsyncProcess(self).OnReadData := @internalDoOnReadData;
end;
procedure TCEProcess.internalDoOnReadData(sender: TObject);
begin
fHasRead := true;
fillOutputStack;
if fRealOnReadData <> nil then
fRealOnReadData(self);
end;
procedure TCEProcess.internalDoOnTerminate(sender: TObject);
begin
fHasRead := false;
fTerminateChecker.Enabled := false;
if fDoneTerminated then exit;
fDoneTerminated := true;
// 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;
// note: redirection is to output stream is done by hand at the end
// because of sync issue (https://github.com/BBasile/Coedit/issues/336).
if fRedirectStdErr then
appendStdErr;
if fRealOnTerminate <> nil then
fRealOnTerminate(self);
end;
procedure TCEProcess.checkTerminated(sender: TObject);
begin
if Running then
exit;
fTerminateChecker.Enabled := false;
internalDoOnTerminate(self);
end;
procedure TCEProcess.blockingWait;
begin
repeat
fillOutputStack;
until
not Running;
checkTerminated(self);
end;
constructor TCEAutoBufferedProcess.create(aOwner: TComponent);
begin
inherited;
fNewBufferTimeOut := 1000;
fNewBufferChecker := TTimer.Create(self);
fNewBufferChecker.Enabled:= false;
fNewBufferChecker.Interval:= fNewBufferTimeOut;
fNewBufferChecker.OnTimer:= @newBufferCheckerChecks;
end;
procedure TCEAutoBufferedProcess.setTimeout(value: integer);
begin
if fNewBufferTimeOut = value then
exit;
fNewBufferTimeOut := value;
fNewBufferChecker.Interval:= fNewBufferTimeOut;
end;
procedure TCEAutoBufferedProcess.execute;
begin
fPreviousSize := fOutputStack.Size;
fNewBufferChecker.Enabled:=true;
inherited;
end;
procedure TCEAutoBufferedProcess.newBufferCheckerChecks(sender: TObject);
begin
if fOutputStack.Size = fPreviousSize then
begin
if assigned(fRealOnReadData) then
fRealOnReadData(self);
end;
fPreviousSize := fOutputStack.Size;
end;
procedure TCEAutoBufferedProcess.internalDoOnReadData(sender: TObject);
begin
fillOutputStack;
end;
procedure TCEAutoBufferedProcess.internalDoOnTerminate(sender: TObject);
begin
fNewBufferChecker.Enabled:=false;
inherited;
end;
end.