added TCEProcess, custom TProcess class for Coedit

- will fix the "split line" issue
- will fix globally the 'OnTerminate' event not called (the timer is always enabled, so osx version is guaranteed not to have this issue)
- will be used widely (no more TProcess or TASyncProcess).
This commit is contained in:
Basile Burg 2015-07-15 14:28:04 +02:00
parent 670ab28b67
commit b911e10e93
4 changed files with 213 additions and 13 deletions

View File

@ -135,7 +135,7 @@
<PackageName Value="LCL"/>
</Item6>
</RequiredPackages>
<Units Count="37">
<Units Count="38">
<Unit0>
<Filename Value="coedit.lpr"/>
<IsPartOfProject Value="True"/>
@ -151,7 +151,6 @@
<Unit3>
<Filename Value="..\src\ce_dcd.pas"/>
<IsPartOfProject Value="True"/>
<UnitName Value="ce_dcd"/>
</Unit3>
<Unit4>
<Filename Value="..\src\ce_dlang.pas"/>
@ -187,7 +186,6 @@
<Unit11>
<Filename Value="..\src\ce_interfaces.pas"/>
<IsPartOfProject Value="True"/>
<UnitName Value="ce_interfaces"/>
</Unit11>
<Unit12>
<Filename Value="..\src\ce_libman.pas"/>
@ -199,7 +197,6 @@
<ComponentName Value="CELibManEditorWidget"/>
<HasResources Value="True"/>
<ResourceBaseClass Value="Form"/>
<UnitName Value="ce_libmaneditor"/>
</Unit13>
<Unit14>
<Filename Value="..\src\ce_main.pas"/>
@ -207,7 +204,6 @@
<ComponentName Value="CEMainForm"/>
<HasResources Value="True"/>
<ResourceBaseClass Value="Form"/>
<UnitName Value="ce_main"/>
</Unit14>
<Unit15>
<Filename Value="..\src\ce_messages.pas"/>
@ -215,7 +211,6 @@
<ComponentName Value="CEMessagesWidget"/>
<HasResources Value="True"/>
<ResourceBaseClass Value="Form"/>
<UnitName Value="ce_messages"/>
</Unit15>
<Unit16>
<Filename Value="..\src\ce_miniexplorer.pas"/>
@ -227,7 +222,6 @@
<Unit17>
<Filename Value="..\src\ce_mru.pas"/>
<IsPartOfProject Value="True"/>
<UnitName Value="ce_mru"/>
</Unit17>
<Unit18>
<Filename Value="..\src\ce_observer.pas"/>
@ -253,12 +247,10 @@
<ComponentName Value="CEProjectConfigurationWidget"/>
<HasResources Value="True"/>
<ResourceBaseClass Value="Form"/>
<UnitName Value="ce_projconf"/>
</Unit21>
<Unit22>
<Filename Value="..\src\ce_nativeproject.pas"/>
<IsPartOfProject Value="True"/>
<UnitName Value="ce_nativeproject"/>
</Unit22>
<Unit23>
<Filename Value="..\src\ce_projinspect.pas"/>
@ -266,7 +258,6 @@
<ComponentName Value="CEProjectInspectWidget"/>
<HasResources Value="True"/>
<ResourceBaseClass Value="Form"/>
<UnitName Value="ce_projinspect"/>
</Unit23>
<Unit24>
<Filename Value="..\src\ce_search.pas"/>
@ -296,7 +287,6 @@
<Unit28>
<Filename Value="..\src\ce_symstring.pas"/>
<IsPartOfProject Value="True"/>
<UnitName Value="ce_symstring"/>
</Unit28>
<Unit29>
<Filename Value="..\src\ce_synmemo.pas"/>
@ -308,7 +298,6 @@
<ComponentName Value="CETodoListWidget"/>
<HasResources Value="True"/>
<ResourceBaseClass Value="Form"/>
<UnitName Value="ce_todolist"/>
</Unit30>
<Unit31>
<Filename Value="..\src\ce_tools.pas"/>
@ -340,6 +329,10 @@
<HasResources Value="True"/>
<ResourceBaseClass Value="Form"/>
</Unit36>
<Unit37>
<Filename Value="..\src\ce_processes.pas"/>
<IsPartOfProject Value="True"/>
</Unit37>
</Units>
</ProjectOptions>
<CompilerOptions>

View File

@ -9,7 +9,7 @@ uses
Interfaces, Forms, lazcontrols, runtimetypeinfocontrols,
ce_sharedres, ce_observer, ce_libman, ce_tools, ce_dcd, ce_main,
ce_writableComponent, ce_symstring, ce_staticmacro, ce_inspectors,
ce_editoroptions, ce_dockoptions, ce_shortcutseditor, ce_mru;
ce_editoroptions, ce_dockoptions, ce_shortcutseditor, ce_mru, ce_processes;
{$R *.res}

View File

@ -722,6 +722,20 @@ begin
//
// note: aList.LoadFromStream() does not work, lines can be split, which breaks message parsing (e.g filename detector).
//
{
Split lines:
------------
The problem comes from TAsynProcess.OnReadData. When the output is read in the
event, it does not always finish on a full line.
Resolution:
-----------
in TAsynProcess.OnReadData Accumulate avalaible output in a stream.
Detects last line terminator in the accumation.
Load TStrings from this stream range.
}
sum := 0;
str := TMemoryStream.Create;
try

193
src/ce_processes.pas Normal file
View File

@ -0,0 +1,193 @@
unit ce_processes;
{$I ce_defines.inc}
interface
uses
Classes, SysUtils, ExtCtrls, process, asyncprocess;
type
{
The common process runner 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.
- 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;
fTerminateChecker: TIdleTimer;
fDoneTerminated: boolean;
procedure checkTerminated(sender: TObject);
procedure setOnTerminate(value: TNotifyEvent);
procedure setOnReadData(value: TNotifyEvent);
procedure internalDoOnReadData(sender: TObject);
procedure internalDoOnTerminate(sender: TObject);
published
property OnTerminate write setOnTerminate;
property OnReadData write setOnReadData;
public
constructor create(aOwner: TComponent); override;
destructor destroy; override;
procedure execute; override;
// 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;
end;
procedure killProcess(var proc: TCEProcess);
implementation
procedure killProcess(var proc: TCEProcess);
begin
if proc = nil then
exit;
if proc.Running then
proc.Terminate(0);
proc.Free;
proc := nil;
end;
constructor TCEProcess.create(aOwner: TComponent);
begin
inherited;
FOutputStack := TMemoryStream.Create;
FTerminateChecker := TIdleTimer.Create(nil);
FTerminateChecker.Interval := 50;
fTerminateChecker.OnTimer := @checkTerminated;
fTerminateChecker.Enabled := false;
fTerminateChecker.AutoEnabled:= true;
TAsyncProcess(self).OnTerminate := @internalDoOnTerminate;
TAsyncProcess(self).OnReadData := @internalDoOnReadData;
end;
destructor TCEProcess.destroy;
begin
FTerminateChecker.Free;
FOutputStack.Free;
inherited;
end;
procedure TCEProcess.Execute;
begin
fOutputStack.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;
sum := fOutputStack.Size;
while (Output <> nil) and (NumBytesAvailable > 0) do
begin
fOutputStack.SetSize(sum + 1024);
cnt := Output.Read((fOutputStack.Memory + sum)^, 1024);
sum += cnt;
end;
fOutputStack.SetSize(sum);
end;
procedure TCEProcess.getFullLines(list: TStrings; consume: boolean = true);
var
stored: Integer;
lastTerm: 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
lastTerm := fOutputStack.Position;
stored := fOutputStack.Position;
while fOutputStack.Read(buff, 1) = 1 do
if buff = 10 then lastTerm := fOutputStack.Position;
fOutputStack.Position := stored;
if lastTerm <> stored then
begin
str := TMemoryStream.Create;
try
toread := lastTerm - 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
fillOutputStack;
if fRealOnReadData <> nil then
fRealOnReadData(self);
end;
procedure TCEProcess.internalDoOnTerminate(sender: TObject);
begin
fTerminateChecker.Enabled := false;
if fDoneTerminated then exit;
fDoneTerminated := true;
//
fillOutputStack;
if fRealOnTerminate <> nil then
fRealOnTerminate(self);
end;
procedure TCEProcess.checkTerminated(sender: TObject);
begin
if Running then
exit;
fTerminateChecker.Enabled := false;
internalDoOnTerminate(self);
end;
end.