From b911e10e9398f43eac410fb66e37eeb21905e480 Mon Sep 17 00:00:00 2001 From: Basile Burg Date: Wed, 15 Jul 2015 14:28:04 +0200 Subject: [PATCH] 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). --- lazproj/coedit.lpi | 17 ++-- lazproj/coedit.lpr | 2 +- src/ce_common.pas | 14 ++++ src/ce_processes.pas | 193 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 213 insertions(+), 13 deletions(-) create mode 100644 src/ce_processes.pas diff --git a/lazproj/coedit.lpi b/lazproj/coedit.lpi index 6a177b4f..0a9baf1a 100644 --- a/lazproj/coedit.lpi +++ b/lazproj/coedit.lpi @@ -135,7 +135,7 @@ - + @@ -151,7 +151,6 @@ - @@ -187,7 +186,6 @@ - @@ -199,7 +197,6 @@ - @@ -207,7 +204,6 @@ - @@ -215,7 +211,6 @@ - @@ -227,7 +222,6 @@ - @@ -253,12 +247,10 @@ - - @@ -266,7 +258,6 @@ - @@ -296,7 +287,6 @@ - @@ -308,7 +298,6 @@ - @@ -340,6 +329,10 @@ + + + + diff --git a/lazproj/coedit.lpr b/lazproj/coedit.lpr index e8ba330e..aab157a5 100644 --- a/lazproj/coedit.lpr +++ b/lazproj/coedit.lpr @@ -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} diff --git a/src/ce_common.pas b/src/ce_common.pas index 6d329361..3ce64b33 100644 --- a/src/ce_common.pas +++ b/src/ce_common.pas @@ -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 diff --git a/src/ce_processes.pas b/src/ce_processes.pas new file mode 100644 index 00000000..bc48a540 --- /dev/null +++ b/src/ce_processes.pas @@ -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. +