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