{ *************************************************************************** * * * This source is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This code is distributed in the hope that it will be useful, but * * WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * * General Public License for more details. * * * * A copy of the GNU General Public License is available on the World * * Wide Web at . You can also * * obtain it by writing to the Free Software Foundation, * * Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1335, USA. * * * *************************************************************************** Author: Mattias Gaertner Abstract: Running external programs and parsing their output lines. } unit ExtTools; {$mode objfpc}{$H+} {off $DEFINE VerboseExtToolErrors} {off $DEFINE VerboseExtToolAddOutputLines} {off $DEFINE VerboseExtToolThread} interface uses // RTL + FCL Classes, SysUtils, Math, process, Pipes, AVL_Tree, System.UITypes, // LazUtils FileUtil, LazFileUtils, LazUtilities, LazLoggerBase, UTF8Process, LazUTF8, AvgLvlTree, // BuildIntf IDEExternToolIntf, BaseIDEIntf, MacroIntf, LazMsgWorker, // IdeUtils IdeUtilsPkgStrConsts, // IdeConfig TransferMacros, // IDE LazarusIDEStrConsts; type TLMVToolState = ( lmvtsRunning, lmvtsSuccess, lmvtsFailed ); TLMVToolStates = set of TLMVToolState; { TLazExtToolView } TLazExtToolView = class(TExtToolView) private FToolState: TLMVToolState; protected procedure SetToolState(AValue: TLMVToolState); virtual; public property ToolState: TLMVToolState read FToolState write SetToolState; end; TExternalTool = class; { TExternalToolThread } TExternalToolThread = class(TThread) private fLines: TStringList; FTool: TExternalTool; procedure SetTool(AValue: TExternalTool); public property Tool: TExternalTool read FTool write SetTool; procedure Execute; override; procedure DebuglnThreadLog(const Args: array of string); destructor Destroy; override; // (main thread) end; { TExternalTool } TExternalTool = class(TAbstractExternalTool) private FThread: TExternalToolThread; fExecuteAfter: TFPList; // list of TExternalTool fExecuteBefore: TFPList; // list of TExternalTool fNeedAfterSync: boolean; fOutputCountNotified: integer; procedure ProcessRunning; // (worker thread) after Process.Execute procedure ProcessStopped; // (worker thread) when process stopped procedure NotifyHandlerStopped; // (main thread) called by ProcessStopped procedure NotifyHandlerNewOutput; // (main thread) called by AddOutputLines procedure SetThread(AValue: TExternalToolThread); // main or worker thread procedure SynchronizedImproveMessages; // (main thread) called by AddOutputLines procedure DoTerminate; // (main thread) protected procedure DoExecute; override; // (main thread) procedure DoStart; // (main thread) procedure CreateView; virtual; abstract; // (main thread) function GetExecuteAfter(Index: integer): TAbstractExternalTool; override; function GetExecuteBefore(Index: integer): TAbstractExternalTool; override; procedure Notification(AComponent: TComponent; Operation: TOperation); override; function CanFree: boolean; override; procedure QueueAsyncAutoFree; virtual; abstract; public constructor Create(aOwner: TComponent); override; destructor Destroy; override; property Thread: TExternalToolThread read FThread write SetThread; procedure Execute; override; // (main thread) procedure Terminate; override; // (main thread) procedure WaitForExit; override; // (main thread) function ResolveMacros: boolean; override; // (main thread) function ExecuteAfterCount: integer; override; function ExecuteBeforeCount: integer; override; procedure RemoveExecuteBefore(Tool: TAbstractExternalTool); override; function IsExecutedBefore(Tool: TAbstractExternalTool): Boolean; override; procedure AddExecuteBefore(Tool: TAbstractExternalTool); override; function CanStart: boolean; function GetLongestEstimatedLoad: int64; procedure UserThreadRunning; override; // (worker thread) when thread Execute procedure UserThreadStopped; override; // (worker thread) when thread stopped procedure AddOutputLines(Lines: TStringList); override; // (worker thread) when new output arrived function InitParsers: boolean; override; end; TExternalToolClass = class of TExternalTool; { TExternalTools } TExternalTools = class(TExternalToolsBase) private FCritSec: TRTLCriticalSection; fRunning: TFPList; // list of TExternalTool, needs Enter/LeaveCriticalSection fOldThreads: TFPList; // list of TExternalToolThread, needs Enter/LeaveCriticalSection FMaxProcessCount: integer; fParsers: TFPList; // list of TExtToolParserClass procedure AddOldThread(aThread: TExternalToolThread); // (main thread) function GetRunningTools(Index: integer): TExternalTool; procedure AddRunningTool(Tool: TExternalTool); // (worker thread) procedure RemoveRunningTool(Tool: TExternalTool); // (worker thread) function RunExtToolHandler(ToolOptions: TIDEExternalToolOptions): boolean; // (main thread) function RunToolAndDetach(ToolOptions: TIDEExternalToolOptions): boolean; // (main thread) function RunToolWithParsers(ToolOptions: TIDEExternalToolOptions): boolean; // (main thread) procedure FreeFinishedThreads; // (main thread) procedure OnThreadTerminate(Sender: TObject); // (main thread) protected FToolClass: TExternalToolClass; function GetParsers(Index: integer): TExtToolParserClass; override; procedure Notification(AComponent: TComponent; Operation: TOperation); override; public constructor Create(aOwner: TComponent); override; destructor Destroy; override; function Add(Title: string): TAbstractExternalTool; override; function IndexOf(Tool: TAbstractExternalTool): integer; override; property MaxProcessCount: integer read FMaxProcessCount write FMaxProcessCount; procedure Work; function FindNextToolToStart: TExternalTool; procedure Terminate(Tool: TExternalTool); procedure TerminateAll; override; procedure Clear; override; function RunningCount: integer; property RunningTools[Index: integer]: TExternalTool read GetRunningTools; procedure EnterCriticalSection; override; procedure LeaveCriticalSection; override; // parsers function ParserCount: integer; override; procedure RegisterParser(Parser: TExtToolParserClass); override; procedure UnregisterParser(Parser: TExtToolParserClass); override; function FindParserForTool(const SubTool: string): TExtToolParserClass; override; function FindParserWithName(const ParserName: string): TExtToolParserClass; override; function GetMsgTool(Msg: TMessageLine): TAbstractExternalTool; override; end; TExternalToolsClass = class of TExternalTools; function ExternalToolsRef: TExternalTools; implementation function ExternalToolsRef: TExternalTools; begin Result := ExternalToolList as TExternalTools; end; {$IF defined(VerboseExtToolErrors) or defined(VerboseExtToolThread) or defined(VerboseExtToolAddOutputLines)} function ArgsToString(Args: array of const): string; var i: Integer; begin Result := ''; for i:=Low(Args) to High(Args) do begin case Args[i].VType of vtInteger: Result := Result + dbgs(Args[i].vinteger); vtInt64: Result := Result + dbgs(Args[i].VInt64^); vtQWord: Result := Result + dbgs(Args[i].VQWord^); vtBoolean: Result := Result + dbgs(Args[i].vboolean); vtExtended: Result := Result + dbgs(Args[i].VExtended^); vtString: Result := Result + Args[i].VString^; vtAnsiString: Result := Result + AnsiString(Args[i].VAnsiString); vtChar: Result := Result + Args[i].VChar; vtPChar: Result := Result + Args[i].VPChar; vtPWideChar: Result := {%H-}Result {%H-}+ Args[i].VPWideChar; vtWideChar: Result := Result + AnsiString(Args[i].VWideChar); vtWidestring: Result := Result + AnsiString(WideString(Args[i].VWideString)); vtObject: Result := Result + DbgSName(Args[i].VObject); vtClass: Result := Result + DbgSName(Args[i].VClass); vtPointer: Result := Result + Dbgs(Args[i].VPointer); else Result := Result + '?unknown variant?'; end; end; end; procedure DebuglnThreadLog(const args: array of const); var s, Filename: string; fs: TFileStream; begin if GetCurrentThreadId=MainThreadID then debugln(args) else begin s:=ArgsToString(args)+sLineBreak; Filename:='lazdbg'+IntToStr(GetCurrentThreadId)+'.log'; if FileExistsUTF8(Filename) then fs:=TFileStream.Create(Filename,fmOpenWrite or fmShareDenyNone) else fs:=TFileStream.Create(Filename,fmCreate); try try fs.Seek(0,soEnd); fs.Write(s[1],length(s)); except end; finally fs.Free; end; end; end; {$ENDIF} { TLazExtToolView } procedure TLazExtToolView.SetToolState(AValue: TLMVToolState); begin if FToolState=AValue then Exit; FToolState:=AValue; end; { TExternalTool } procedure TExternalTool.ProcessRunning; var i: Integer; begin EnterCriticalSection; try if FStage<>etsStarting then exit; FStage:=etsRunning; finally LeaveCriticalSection; end; for i:=0 to ParserCount-1 do Parsers[i].InitReading; end; procedure TExternalTool.ProcessStopped; var i: Integer; begin {$IFDEF VerboseExtToolErrors} if ErrorMessage<>'' then DebuglnThreadLog(['TExternalTool.ThreadStopped ',Title,' ErrorMessage=',ErrorMessage]); {$ENDIF} if Thread<>nil then Thread.Tool:=nil; EnterCriticalSection; try if (not Terminated) and (ErrorMessage='') then begin if ExitCode<>0 then ErrorMessage:=Format(lisExitCode, [IntToStr(ExitCode)]) else if ExitStatus<>0 then ErrorMessage:='ExitStatus '+IntToStr(ExitStatus); end; if FStage>=etsStopped then exit; FStage:=etsStopped; finally LeaveCriticalSection; end; for i:=0 to ParserCount-1 do begin try Parsers[i].Done; except on E: Exception do begin {$IFDEF VerboseExtToolErrors} DebuglnThreadLog(['TExternalTool.ProcessStopped ',Title,' Error in ',DbgSName(Parsers[i]),': ',E.Message]); {$ENDIF} end; end; end; if Tools<>nil then TExternalTools(Tools).RemoveRunningTool(Self); TThread.Synchronize(nil,@NotifyHandlerStopped); end; procedure TExternalTool.AddOutputLines(Lines: TStringList); var i: Integer; Handled: Boolean; Line: LongInt; OldOutputCount: LongInt; OldMsgCount: LongInt; Parser: TExtToolParser; NeedSynchronize, IsStdErr: Boolean; MsgLine: TMessageLine; LineStr: String; begin {$IFDEF VerboseExtToolAddOutputLines} DebuglnThreadLog(['TExternalTool.AddOutputLines ',Title,' Tick=',IntToStr(GetTickCount64),' Lines=',Lines.Count]); {$ENDIF} if (Lines=nil) or (Lines.Count=0) then exit; NeedSynchronize:=false; EnterCriticalSection; try OldOutputCount:=WorkerOutput.Count; OldMsgCount:=WorkerMessages.Count; WorkerOutput.AddStrings(Lines); for i:=0 to ParserCount-1 do Parsers[i].NeedSynchronize:=false; // feed new lines into all parsers, converting raw lines into messages for Line:=OldOutputCount to WorkerOutput.Count-1 do begin Handled:=false; LineStr:=WorkerOutput[Line]; IsStdErr:=WorkerOutput.Objects[Line]<>nil; for i:=0 to ParserCount-1 do begin {$IFDEF VerboseExtToolAddOutputLines} DebuglnThreadLog(['TExternalTool.AddOutputLines ',DbgSName(Parsers[i]),' Line="',WorkerOutput[Line],'" READLINE ...']); {$ENDIF} Parsers[i].ReadLine(LineStr,Line,IsStdErr,Handled); if Handled then break; end; if (not Handled) then begin MsgLine:=WorkerMessages.CreateLine(Line); MsgLine.Msg:=LineStr; // use raw output as default msg if ParserCount=0 then MsgLine.Urgency:=mluImportant else MsgLine.Urgency:=mluDebug; if IsStdErr then MsgLine.Flags:=MsgLine.Flags+[mlfStdErr]; WorkerMessages.Add(MsgLine); end; end; // let all parsers improve the new messages in worker thread before synchronized if OldMsgCountnil then Thread.Synchronize(Thread,@SynchronizedImproveMessages) else if UserThread<>nil then TThread.Synchronize(UserThread,@SynchronizedImproveMessages) end; EnterCriticalSection; try // let all parsers improve the new messages in worker thread after synchronized if fNeedAfterSync then begin for i:=0 to ParserCount-1 do begin Parser:=Parsers[i]; if not Parser.NeedAfterSync then continue; {$IFDEF VerboseExtToolAddOutputLines} DebuglnThreadLog(['TExternalTool.AddOutputLines ',DbgSName(Parser),' IMPROVE after sync ...']); {$ENDIF} Parser.ImproveMessages(etpspAfterSync); end; end; // feed new messages into all viewers if OldMsgCount0{$ENDIF} then begin {$IFDEF VerboseExtToolAddOutputLines} DebuglnThreadLog(['TExternalTool.AddOutputLines NotifyHandlerNewOutput ...']); {$ENDIF} Thread.Synchronize(Thread,@NotifyHandlerNewOutput); end; fOutputCountNotified:=WorkerOutput.Count; {$IFDEF VerboseExtToolAddOutputLines} DebuglnThreadLog(['TExternalTool.AddOutputLines END']); {$ENDIF} end; function TExternalTool.InitParsers: boolean; var i: Integer; aParser: TExtToolParser; begin for i:=0 to ParserCount-1 do begin aParser:=Parsers[i]; try aParser.Init; except on E: Exception do begin ErrorMessage:=Format(lisParser, [DbgSName(aParser), E.Message]); if (FStage>=etsStopped) then exit(true); debugln(['Error: (lazarus) [TExternalTool.InitParsers] Error=',ErrorMessage]); EnterCriticalSection; try if FStage>=etsStopped then exit(true); FStage:=etsStopped; finally LeaveCriticalSection; end; exit; end; end; end; Result:=true; end; procedure TExternalTool.NotifyHandlerStopped; var i: Integer; View: TExtToolView; begin DoCallNotifyHandler(ethStopped); EnterCriticalSection; try for i:=ViewCount-1 downto 0 do begin if i>=ViewCount then continue; View:=Views[i]; if ErrorMessage<>'' then View.SummaryMsg:=ErrorMessage else View.SummaryMsg:=lisSuccess; View.InputClosed; // this might delete the view end; finally LeaveCriticalSection; end; if Group<>nil then Group.ToolExited(Self); // process stopped => start next if Tools<>nil then TExternalTools(Tools).Work; // free tool if not used AutoFree; end; procedure TExternalTool.NotifyHandlerNewOutput; var i: integer; begin if fOutputCountNotified>=WorkerOutput.Count then exit; {$IFDEF VerboseExtToolAddOutputLines} for i:=fOutputCountNotified to WorkerOutput.Count-1 do debugln('IDE-DEBUG: ',WorkerOutput[i]); {$ENDIF} i:=FHandlers[ethNewOutput].Count; while FHandlers[ethNewOutput].NextDownIndex(i) do TExternalToolNewOutputEvent(FHandlers[ethNewOutput][i])(Self,fOutputCountNotified); end; procedure TExternalTool.SetThread(AValue: TExternalToolThread); var OldThread: TExternalToolThread; begin if FThread=AValue then Exit; OldThread:=FThread; FThread:=AValue; if OldThread<>nil then OldThread.Tool:=nil; if FThread<>nil then FThread.Tool:=Self; end; procedure TExternalTool.SynchronizedImproveMessages; var i: Integer; Parser: TExtToolParser; begin EnterCriticalSection; try fNeedAfterSync:=false; for i:=0 to ParserCount-1 do begin Parser:=Parsers[i]; if not Parser.NeedSynchronize then continue; {$IFDEF VerboseExtToolAddOutputLines} //debugln(['TExternalTool.SynchronizedImproveMessages ',DbgSName(Parser),' ...']); {$ENDIF} Parser.ImproveMessages(etpspSynchronized); Parser.NeedSynchronize:=false; if Parser.NeedAfterSync then fNeedAfterSync:=true; end; finally LeaveCriticalSection; end; end; constructor TExternalTool.Create(aOwner: TComponent); begin inherited Create(aOwner); FWorkerOutput:=TStringList.Create; FProcess:=TProcessUTF8.Create(nil); FProcess.Options:= [poUsePipes{$IFDEF Windows},poStderrToOutPut{$ENDIF}]; FProcess.ShowWindow := swoHide; fExecuteBefore:=TFPList.Create; fExecuteAfter:=TFPList.Create; end; destructor TExternalTool.Destroy; var OldThread: TExternalToolThread; begin //debugln(['TExternalTool.Destroy ',Title]); EnterCriticalSection; try FStage:=etsDestroying; if Thread is TExternalToolThread then begin OldThread:=TExternalToolThread(Thread); fThread:=nil; OldThread.Tool:=nil; end; FreeAndNil(FProcess); FreeAndNil(FWorkerOutput); FreeAndNil(fExecuteBefore); FreeAndNil(fExecuteAfter); finally LeaveCriticalSection; end; inherited Destroy; end; procedure TExternalTool.DoExecute; // in main thread function CheckError: boolean; begin if (FStage>=etsStopped) then exit(true); if (ErrorMessage='') then exit(false); debugln(['Error: (lazarus) [TExternalTool.DoExecute.CheckError] Error=',ErrorMessage]); EnterCriticalSection; try if FStage>=etsStopped then exit(true); FStage:=etsStopped; finally LeaveCriticalSection; end; CreateView; NotifyHandlerStopped; Result:=true; end; var ExeFile: String; i: Integer; aParser: TExtToolParser; begin if Terminated then exit; // set Stage to etsInitializing EnterCriticalSection; try if Stage<>etsInit then raise Exception.Create('TExternalTool.Execute: already initialized'); FStage:=etsInitializing; finally LeaveCriticalSection; end; // resolve macros if ResolveMacrosOnExecute then begin if not ResolveMacros then begin if ErrorMessage='' then ErrorMessage:=lisFailedToResolveMacros; if CheckError then exit; end; end; // init CurrentDirectory Process.CurrentDirectory:=TrimFilename(Process.CurrentDirectory); if not FilenameIsAbsolute(Process.CurrentDirectory) then Process.CurrentDirectory:=AppendPathDelim(GetCurrentDirUTF8)+Process.CurrentDirectory; // init Executable Process.Executable:=TrimFilename(Process.Executable); {$IFDEF VerboseExtToolThread} debugln(['TExternalTool.DoExecute Exe=',Process.Executable]); {$ENDIF} if not FilenameIsAbsolute(Process.Executable) then begin if ExtractFilePath(Process.Executable)<>'' then Process.Executable:=AppendPathDelim(GetCurrentDirUTF8)+Process.Executable else if Process.Executable='' then begin ErrorMessage:=Format(lisToolHasNoExecutable, [Title]); CheckError; exit; end else begin ExeFile:=FindDefaultExecutablePath(Process.Executable,GetCurrentDirUTF8); if ExeFile='' then begin ErrorMessage:=Format(lisCanNotFindExecutable, [Process.Executable]); CheckError; exit; end; Process.Executable:=ExeFile; end; end; ExeFile:=Process.Executable; if not FileExistsUTF8(ExeFile) then begin ErrorMessage:=Format(lisMissingExecutable, [ExeFile]); CheckError; exit; end; if DirectoryExistsUTF8(ExeFile) then begin ErrorMessage:=Format(lisExecutableIsADirectory, [ExeFile]); CheckError; exit; end; if not FileIsExecutable(ExeFile) then begin ErrorMessage:=Format(lisExecutableLacksThePermissionToRun, [ExeFile]); CheckError; exit; end; // init misc WorkerMessages.BaseDirectory:=Process.CurrentDirectory; WorkerDirectory:=WorkerMessages.BaseDirectory; if EnvironmentOverrides.Count>0 then AssignEnvironmentTo(Process.Environment,EnvironmentOverrides); // init parsers for i:=0 to ParserCount-1 do begin aParser:=Parsers[i]; try aParser.Init; except on E: Exception do begin ErrorMessage:=Format(lisParser, [DbgSName(aParser), E.Message]); CheckError; exit; end; end; end; // set Stage to etsWaitingForStart EnterCriticalSection; try if Stage<>etsInitializing then raise Exception.Create('TExternalTool.Execute: bug in initialization'); FStage:=etsWaitingForStart; finally LeaveCriticalSection; end; end; procedure TExternalTool.DoStart; var i: Integer; begin // set Stage to etsStarting EnterCriticalSection; try if Stage<>etsWaitingForStart then raise Exception.Create('TExternalTool.Execute: already started'); FStage:=etsStarting; finally LeaveCriticalSection; end; CreateView; // mark running if Tools<>nil then TExternalTools(Tools).AddRunningTool(Self); // start thread if Thread=nil then begin FThread:=TExternalToolThread.Create(true); Thread.Tool:=Self; FThread.FreeOnTerminate:=false; FThread.OnTerminate:=@TExternalTools(Tools).OnThreadTerminate; end; if ConsoleVerbosity>=0 then begin debugln(['Info: (lazarus) Execute Title="',Title,'"']); debugln(['Info: (lazarus) Working Directory="',Process.CurrentDirectory,'"']); debugln(['Info: (lazarus) Executable="',Process.Executable,'"']); for i:=0 to Process.Parameters.Count-1 do debugln(['Info: (lazarus) Param[',i,']="',Process.Parameters[i],'"']); for i:=0 to EnvironmentOverrides.Count-1 do debugln(['Info: (lazarus) Env[',i,']="',LeftStr(DbgStr(EnvironmentOverrides[i]),100),'"']); end; Thread.Start; end; function TExternalTool.ExecuteBeforeCount: integer; begin Result:=fExecuteBefore.Count; end; function TExternalTool.ExecuteAfterCount: integer; begin Result:=fExecuteAfter.Count; end; function TExternalTool.GetExecuteAfter(Index: integer): TAbstractExternalTool; begin Result:=TAbstractExternalTool(fExecuteAfter[Index]); end; function TExternalTool.GetExecuteBefore(Index: integer): TAbstractExternalTool; begin Result:=TAbstractExternalTool(fExecuteBefore[Index]); end; procedure TExternalTool.DoTerminate; var NeedProcTerminate: Boolean; begin NeedProcTerminate:=false; EnterCriticalSection; try if ConsoleVerbosity>0 then DebugLn(['Info: (lazarus) TExternalTool.DoTerminate ',Title,', Terminated=',Terminated,', Stage=',dbgs(Stage)]); if Terminated then exit; if Stage=etsStopped then exit; if ErrorMessage='' then ErrorMessage:=lisAborted; fTerminated:=true; if Stage=etsRunning then NeedProcTerminate:=true; if Stagenil) then begin if ConsoleVerbosity>0 then DebugLn(['Info: (lazarus) TExternalTool.DoTerminate ',Title,'. Terminating the process.']); Process.Terminate(AbortedExitCode); end; end; procedure TExternalTool.Notification(AComponent: TComponent; Operation: TOperation); begin inherited Notification(AComponent, Operation); if Operation=opRemove then begin if fExecuteBefore<>nil then fExecuteBefore.Remove(AComponent); if fExecuteAfter<>nil then fExecuteAfter.Remove(AComponent); end; end; function TExternalTool.CanFree: boolean; begin Result:=(FThread=nil) and inherited CanFree; end; function TExternalTool.IsExecutedBefore(Tool: TAbstractExternalTool): Boolean; var Visited: TFPList; function Search(CurTool: TAbstractExternalTool): Boolean; var i: Integer; begin if CurTool=Tool then exit(true); if Visited.IndexOf(CurTool)>=0 then exit(false); Visited.Add(CurTool); for i:=0 to CurTool.ExecuteBeforeCount-1 do if Search(CurTool.ExecuteBefore[i]) then exit(true); Result:=false; end; begin Result:=false; if Tool=Self then exit; Visited:=TFPList.Create; try Result:=Search(Self); finally Visited.Free; end; end; procedure TExternalTool.AddExecuteBefore(Tool: TAbstractExternalTool); begin //debugln(['TExternalTool.AddExecuteBefore Self=',Title,' Tool=',Tool.Title]); if (Tool=Self) or (Tool.IsExecutedBefore(Self)) then raise Exception.Create('TExternalTool.AddExecuteBefore: that would create a circle'); if (fExecuteBefore<>nil) and (fExecuteBefore.IndexOf(Tool)<0) then fExecuteBefore.Add(Tool); if (TExternalTool(Tool).fExecuteAfter<>nil) and (TExternalTool(Tool).fExecuteAfter.IndexOf(Self)<=0) then TExternalTool(Tool).fExecuteAfter.Add(Self); end; function TExternalTool.CanStart: boolean; var i: Integer; ExecBefore: TAbstractExternalTool; begin Result:=false; //debugln(['TExternalTool.CanStart ',Title,' ',dbgs(Stage)]); if Stage<>etsWaitingForStart then exit; if Terminated then exit; for i:=0 to ExecuteBeforeCount-1 do begin ExecBefore:=ExecuteBefore[i]; if ord(ExecBefore.Stage)'' then exit; end; Result:=true; end; function TExternalTool.GetLongestEstimatedLoad: int64; type TInfo = record Load: int64; end; PInfo = ^TInfo; var ToolToInfo: TPointerToPointerTree; function GetLoad(Tool: TExternalTool): int64; var Info: PInfo; i: Integer; begin Info:=PInfo(ToolToInfo[Tool]); if Info<>nil then Result:=Info^.Load else begin New(Info); Info^.Load:=1; ToolToInfo[Tool]:=Info; Result:=0; for i:=0 to Tool.ExecuteAfterCount-1 do Result:=Max(Result,GetLoad(TExternalTool(Tool.ExecuteAfter[i]))); inc(Result,Tool.EstimatedLoad); Info^.Load:=Result; end; end; var Node: TAvlTreeNode; Item: PPointerToPointerItem; Info: PInfo; begin ToolToInfo:=TPointerToPointerTree.Create; try Result:=GetLoad(Self); finally Node:=ToolToInfo.Tree.FindLowest; while Node<>nil do begin Item:=PPointerToPointerItem(Node.Data); Info:=PInfo(Item^.Value); Dispose(Info); Node:=ToolToInfo.Tree.FindSuccessor(Node); end; ToolToInfo.Free; end; end; procedure TExternalTool.UserThreadRunning; var i: Integer; begin EnterCriticalSection; try if FStage>etsRunning then exit; FStage:=etsRunning; finally LeaveCriticalSection; end; for i:=0 to ParserCount-1 do Parsers[i].InitReading; end; procedure TExternalTool.UserThreadStopped; begin ProcessStopped; end; procedure TExternalTool.Execute; begin if Stage<>etsInit then raise Exception.Create('TExternalTool.Execute "'+Title+'" already started'); DoExecute; if Stage<>etsWaitingForStart then exit; if Tools<>nil then TExternalTools(Tools).Work else DoStart; end; procedure TExternalTool.Terminate; begin if Tools<>nil then TExternalTools(Tools).Terminate(Self) else DoTerminate; end; procedure TExternalTool.WaitForExit; var MyTools: TExternalToolsBase; begin MyTools:=Tools; repeat EnterCriticalSection; try if Stage=etsDestroying then exit; if (Stage=etsStopped) and (FindUnfinishedView=nil) then exit; finally LeaveCriticalSection; end; // call synchronized tasks, this might free this tool if MainThreadID=ThreadID then begin Assert(Owner is TExternalToolsBase, 'TExternalTool.WaitForExit: Owner is not TExternalToolsBase.'); TExternalToolsBase(Owner).HandleMessages; end; Assert(Assigned(ExternalToolList), 'TExternalTool.WaitForExit: ExternalToolList=Nil.'); // check if this tool still exists if MyTools.IndexOf(Self)<0 then exit; // still running => wait Sleep(50); until false; end; function TExternalTool.ResolveMacros: boolean; function Resolve(const aValue: string; out NewValue: string): boolean; begin NewValue:=aValue; Result:=IDEMacros.SubstituteMacros(NewValue); if Result then exit; if ErrorMessage='' then ErrorMessage:=Format(lisInvalidMacrosIn, [aValue]); LazMessageWorker(lisCCOErrorCaption, Format(lisInvalidMacrosInExternalTool, [aValue, Title]), mtError,[mbCancel]); end; var i: Integer; s: string; begin if IDEMacros=nil then exit(true); Result:=false; if not Resolve(Process.CurrentDirectory,s) then exit; Process.CurrentDirectory:=s; if not Resolve(Process.Executable,s) then exit; Process.Executable:=s; for i:=0 to Process.Parameters.Count-1 do begin if not Resolve(Process.Parameters[i],s) then exit; Process.Parameters[i]:=s; end; for i:=0 to EnvironmentOverrides.Count-1 do begin if not Resolve(EnvironmentOverrides[i],s) then exit; EnvironmentOverrides[i]:=s; end; Result:=true; end; procedure TExternalTool.RemoveExecuteBefore(Tool: TAbstractExternalTool); begin if fExecuteBefore<>nil then fExecuteBefore.Remove(Tool); if TExternalTool(Tool).fExecuteAfter<>nil then TExternalTool(Tool).fExecuteAfter.Remove(Self); end; { TExternalTools } function TExternalTools.RunExtToolHandler(ToolOptions: TIDEExternalToolOptions): boolean; begin {$IFDEF VerboseExtToolThread} debugln(['TExternalTools.RunExtToolHandler ',ToolOptions.Title, ' exe="',ToolOptions.Executable,'" params="',ToolOptions.CmdLineParams,'"']); {$ENDIF} if ToolOptions.Parsers.Count=0 then Result := RunToolAndDetach(ToolOptions) else Result := RunToolWithParsers(ToolOptions) end; function TExternalTools.RunToolAndDetach(ToolOptions: TIDEExternalToolOptions): boolean; // simply run and detach procedure Err(Msg: string); begin debugln(['Error: (lazarus) [TExternalTools.RunToolAndDetach] ',ToolOptions.Title,' failed: ',Msg]); // ToDo: show user end; var i: Integer; Proc: TProcessUTF8; sl: TStringList; s, Path: String; begin Result:=false; Proc:=TProcessUTF8.Create(nil); try Proc.InheritHandles:=false; // working directory s:=ToolOptions.WorkingDirectory; if ToolOptions.ResolveMacros then begin if not GlobalMacroList.SubstituteStr(s) then begin Err('macros of WorkingDirectory: "'+ToolOptions.WorkingDirectory+'"'); exit; end; end; s:=ChompPathDelim(CleanAndExpandDirectory(s)); if not DirectoryExistsUTF8(s) then begin Err('missing directory "'+s+'"'); exit; end; Proc.CurrentDirectory:=s; // environment if ToolOptions.EnvironmentOverrides.Count>0 then AssignEnvironmentTo(Proc.Environment,ToolOptions.EnvironmentOverrides); if ToolOptions.ResolveMacros then begin for i:=0 to Proc.Environment.Count-1 do begin s:=Proc.Environment[i]; if not GlobalMacroList.SubstituteStr(s) then begin Err('environment override "'+Proc.Environment[i]+'"'); exit; end; Proc.Environment[i]:=s; end; end; // executable s:=ToolOptions.Executable; if ToolOptions.ResolveMacros then begin if not GlobalMacroList.SubstituteStr(s) then begin Err('invalid macros in Executable: "'+ToolOptions.Executable+'"'); exit; end; end; if not FilenameIsAbsolute(s) then begin // search in PATH if Proc.Environment.Count>0 then Path:=Proc.Environment.Values['PATH'] else Path:=GetEnvironmentVariableUTF8('PATH'); s:=SearchFileInPath(s,Proc.CurrentDirectory, Path, PathSeparator, sffFindProgramInPath); {$IFDEF Windows} if (s='') and (ExtractFileExt(s)='') then begin s:=SearchFileInPath(s+'.exe',Proc.CurrentDirectory, Path, PathSeparator, sffFindProgramInPath); end; {$ENDIF} if s='' then begin Err('missing executable "'+ToolOptions.Executable+'"'); exit; end; end; if not FilenameIsAbsolute(s) then begin Err('missing executable: "'+s+'"'); exit; end; if not FileExistsUTF8(s) then begin Err('missing executable: "'+s+'"'); exit; end; if DirectoryExistsUTF8(s) {$IFDEF DARWIN}and (ExtractFileExt(s)<>'.app'){$ENDIF} then begin Err('executable is a directory: "'+s+'"'); exit; end; if {$IFDEF DARWIN}(ExtractFileExt(s)<>'.app') and{$ENDIF} not FileIsExecutable(s) then begin Err('executable lacks permission to run: "'+s+'"'); exit; end; {$IFDEF DARWIN} if DirectoryExistsUTF8(s) then begin Proc.Executable:='/usr/bin/open'; s:=s+LineEnding+ToolOptions.CmdLineParams; end else {$ENDIF} begin Proc.Executable:=s; s:=ToolOptions.CmdLineParams; end; // params if ToolOptions.ResolveMacros and not GlobalMacroList.SubstituteStr(s) then begin Err('invalid macros in cmd line params "'+ToolOptions.CmdLineParams+'"'); exit; end; sl:=TStringList.Create; try SplitCmdLineParams(s,sl); Proc.Parameters:=sl; finally sl.Free; end; // run and detach if ToolOptions.ShowConsole then Proc.Options:=Proc.Options+[poNewConsole]-[poNoConsole] else Proc.Options:=Proc.Options-[poNewConsole]+[poNoConsole]; if ToolOptions.HideWindow then Proc.ShowWindow:=swoHide else Proc.ShowWindow:=swoShow; try Proc.Execute; Result:=true; except end; finally Proc.Free; end; end; function TExternalTools.RunToolWithParsers(ToolOptions: TIDEExternalToolOptions): boolean; // run with parsers and messages var Tool: TAbstractExternalTool; i: Integer; begin {$IFDEF VerboseExtToolThread} debugln(['TExternalTools.RunToolWithParsers run with scanners ...']); {$ENDIF} Result:=false; Tool:=Add(ToolOptions.Title); Tool.Reference(Self,ClassName); try Tool.Hint:=ToolOptions.Hint; Tool.Process.CurrentDirectory:=ToolOptions.WorkingDirectory; Tool.Process.Executable:=ToolOptions.Executable; Tool.CmdLineParams:=ToolOptions.CmdLineParams; Tool.EnvironmentOverrides:=ToolOptions.EnvironmentOverrides; Assert(Assigned(ToolOptions.Parsers), 'TExternalTools.RunToolWithParsers: Parsers=Nil.'); for i:=0 to ToolOptions.Parsers.Count-1 do Tool.AddParsers(ToolOptions.Parsers[i]); if ToolOptions.ShowConsole then Tool.Process.Options:=Tool.Process.Options+[poNewConsole]-[poNoConsole] else Tool.Process.Options:=Tool.Process.Options-[poNewConsole]+[poNoConsole]; if ToolOptions.HideWindow then Tool.Process.ShowWindow:=swoHide else Tool.Process.ShowWindow:=swoShow; Tool.MaxIdleInMS:=ToolOptions.MaxIdleInMS; if ToolOptions.ResolveMacros and not Tool.ResolveMacros then begin debugln(['Error: (lazarus) [TExternalTools.RunToolWithParsers] failed to resolve macros']); exit; end; {$IFDEF VerboseExtToolThread} debugln(['TExternalTools.RunToolWithParsers Execute ',Tool.Title,' WD="',Tool.Process.CurrentDirectory,'" Exe="',Tool.Process.Executable,'" Params="',Tool.CmdLineParams,'" ...']); {$ENDIF} Tool.Execute; {$IFDEF VerboseExtToolThread} debugln(['TExternalTools.RunToolWithParsers WaitForExit ',Tool.Title,' ...']); {$ENDIF} Tool.WaitForExit; {$IFDEF VerboseExtToolThread} debugln(['TExternalTools.RunToolWithParsers Done ',Tool.Title]); {$ENDIF} Result:=(Tool.ErrorMessage='') and (not Tool.Terminated) and (Tool.ExitStatus=0); finally Tool.Release(Self); end; end; procedure TExternalTools.FreeFinishedThreads; var i: Integer; aThread: TExternalToolThread; begin for i:=fOldThreads.Count-1 downto 0 do begin aThread:=TExternalToolThread(fOldThreads[i]); if aThread.Finished then begin fOldThreads.Delete(i); aThread.Free; end; end; end; procedure TExternalTools.OnThreadTerminate(Sender: TObject); begin AddOldThread(TExternalToolThread(Sender)); end; procedure TExternalTools.AddOldThread(aThread: TExternalToolThread); var OldTool: TExternalTool; begin OldTool:=aThread.Tool; aThread.Tool:=nil; if fOldThreads.IndexOf(aThread)<0 then fOldThreads.Add(aThread); if OldTool<>nil then OldTool.AutoFree; end; function TExternalTools.GetRunningTools(Index: integer): TExternalTool; begin EnterCriticalSection; try Result:=TExternalTool(fRunning[Index]); finally LeaveCriticalSection; end; end; procedure TExternalTools.AddRunningTool(Tool: TExternalTool); begin EnterCriticalSection; try if fRunning.IndexOf(Tool)<0 then fRunning.Add(Tool); finally LeaveCriticalSection; end; end; procedure TExternalTools.RemoveRunningTool(Tool: TExternalTool); begin EnterCriticalSection; try fRunning.Remove(Tool); finally LeaveCriticalSection; end; end; function TExternalTools.GetParsers(Index: integer): TExtToolParserClass; begin Result:=TExtToolParserClass(fParsers[Index]); end; procedure TExternalTools.Notification(AComponent: TComponent; Operation: TOperation); begin inherited Notification(AComponent, Operation); if Operation=opRemove then begin EnterCriticalSection; try if fItems<>nil then fItems.Remove(AComponent); if fRunning<>nil then fRunning.Remove(AComponent); finally LeaveCriticalSection; end; end; end; constructor TExternalTools.Create(aOwner: TComponent); begin inherited Create(aOwner); InitCriticalSection(FCritSec); fRunning:=TFPList.Create; fParsers:=TFPList.Create; fOldThreads:=TFPList.Create; MaxProcessCount:=DefaultMaxProcessCount; RunExternalTool := @RunExtToolHandler; end; destructor TExternalTools.Destroy; begin RunExternalTool:=nil; TerminateAll; EnterCriticalSection; try if fRunning.Count>0 then raise Exception.Create('TExternalTools.Destroy some tools still running'); inherited Destroy; FreeAndNil(fRunning); FreeAndNil(fParsers); FreeAndNil(fOldThreads); finally LeaveCriticalSection; end; DoneCriticalsection(FCritSec); end; function TExternalTools.Add(Title: string): TAbstractExternalTool; begin Result:=FToolClass.Create(Self); Result.Title:=Title; fItems.Add(Result); end; function TExternalTools.IndexOf(Tool: TAbstractExternalTool): integer; begin Result:=fItems.IndexOf(Tool); end; function TExternalTools.ParserCount: integer; begin Result:=fParsers.Count; end; procedure TExternalTools.Work; var Tool: TExternalTool; begin while RunningCountnil) and (Load>=CurLoad) then Continue; Result:=Tool; Load:=CurLoad; end; end; procedure TExternalTools.Terminate(Tool: TExternalTool); begin if Tool=nil then exit; Tool.DoTerminate; end; procedure TExternalTools.TerminateAll; // terminate all current tools var i: Integer; begin for i:=Count-1 downto 0 do Terminate(Items[i] as TExternalTool); FreeFinishedThreads; end; procedure TExternalTools.Clear; begin TerminateAll; while Count>0 do Items[0].Free; end; function TExternalTools.RunningCount: integer; begin Result:=fRunning.Count; end; procedure TExternalTools.EnterCriticalSection; begin System.EnterCriticalsection(FCritSec); end; procedure TExternalTools.LeaveCriticalSection; begin System.LeaveCriticalsection(FCritSec); end; procedure TExternalTools.RegisterParser(Parser: TExtToolParserClass); begin if fParsers.IndexOf(Parser)>=0 then exit; fParsers.Add(Parser); end; procedure TExternalTools.UnregisterParser(Parser: TExtToolParserClass); begin if fParsers=nil then exit; fParsers.Remove(Parser); end; function TExternalTools.FindParserForTool(const SubTool: string): TExtToolParserClass; var i: Integer; begin for i:=0 to fParsers.Count-1 do begin Result:=TExtToolParserClass(fParsers[i]); if Result.CanParseSubTool(SubTool) then exit; end; Result:=nil; end; function TExternalTools.FindParserWithName(const ParserName: string): TExtToolParserClass; var i: Integer; begin for i:=0 to fParsers.Count-1 do begin Result:=TExtToolParserClass(fParsers[i]); if SameText(Result.GetParserName,ParserName) then exit; end; Result:=nil; end; function TExternalTools.GetMsgTool(Msg: TMessageLine): TAbstractExternalTool; var CurOwner: TObject; View: TExtToolView; begin Result:=nil; if (Msg=nil) or (Msg.Lines=nil) then exit; CurOwner:=Msg.Lines.Owner; if CurOwner=nil then exit; if CurOwner is TAbstractExternalTool then Result:=TAbstractExternalTool(CurOwner) else if CurOwner is TExtToolView then begin View:=TExtToolView(CurOwner); Result:=View.Tool; end; end; { TExternalToolThread } procedure TExternalToolThread.SetTool(AValue: TExternalTool); var OldTool: TExternalTool; begin if FTool=AValue then Exit; OldTool:=FTool; FTool:=nil; if OldTool<>nil then OldTool.Thread:=nil; if AValue<>nil then begin FTool:=AValue; if FTool<>nil then FTool.Thread:=Self; end; end; procedure TExternalToolThread.Execute; type TErrorFrame = record Addr: Pointer; Line: shortstring; end; PErrorFrame = ^TErrorFrame; var ErrorFrames: array[0..30] of TErrorFrame; ErrorFrameCount: integer; function GetExceptionStackTrace: string; var FrameCount: LongInt; Frames: PPointer; Cnt: LongInt; f: PErrorFrame; i: Integer; begin Result:=''; FrameCount:=ExceptFrameCount; Frames:=ExceptFrames; ErrorFrames[0].Addr:=ExceptAddr; ErrorFrames[0].Line:=''; ErrorFrameCount:=1; Cnt:=FrameCount; for i:=1 to Cnt do begin ErrorFrames[i].Addr:=Frames[i-1]; ErrorFrames[i].Line:=''; ErrorFrameCount:=i+1; end; for i:=0 to ErrorFrameCount-1 do begin f:=@ErrorFrames[i]; try f^.Line:=copy(BackTraceStrFunc(f^.Addr),1,255); except f^.Line:=copy(SysBackTraceStr(f^.Addr),1,255); end; end; for i:=0 to ErrorFrameCount-1 do begin Result+=ErrorFrames[i].Line+LineEnding; end; end; var Buf: string; function ReadInputPipe(aStream: TInputPipeStream; var LineBuf: string; IsStdErr: boolean): boolean; // true if some bytes have been read var Count: DWord; StartPos: Integer; i: DWord; begin Result:=false; if aStream=nil then exit; Count:=aStream.NumBytesAvailable; if Count=0 then exit; Count:=aStream.Read(Buf[1],Min(length(Buf),Count)); if Count=0 then exit; Result:=true; StartPos:=1; i:=1; while i<=Count do begin if Buf[i] in [#10,#13] then begin LineBuf:=LineBuf+copy(Buf,StartPos,i-StartPos); if IsStdErr then fLines.AddObject(LineBuf,fLines) else fLines.Add(LineBuf); LineBuf:=''; if (iBuf[i+1]) then inc(i); StartPos:=i+1; end; inc(i); end; LineBuf:=LineBuf+copy(Buf,StartPos,Count-StartPos+1); end; const UpdateTimeDiff = 1000 div 5; // update five times a second, even if there is still work var {$IFDEF VerboseExtToolThread} Title: String; {$ENDIF} OutputLine, StdErrLine: String; LastUpdate: QWord; ErrMsg: String; ok: Boolean; HasOutput: Boolean; IdleCount: Integer; begin {$IFDEF VerboseExtToolThread} Title:=Tool.Title; {$ENDIF} if Tool.Thread<>Self then raise Exception.Create(''); SetLength(Buf{%H-},4096); ErrorFrameCount:=0; fLines:=TStringList.Create; try try if Tool.Stage<>etsStarting then begin {$IFDEF VerboseExtToolThread} DebuglnThreadLog(['TExternalToolThread.Execute ',Title,' Tool.Stage=',dbgs(Tool.Stage),' aborting']); {$ENDIF} exit; end; {$IFDEF VerboseExtToolThread} DebuglnThreadLog(['TExternalToolThread.Execute ',Title,' check executing "',Tool.Process.Executable,'" ...']); {$ENDIF} if not FileIsExecutable(Tool.Process.Executable) then begin Tool.ErrorMessage:=Format(lisCanNotExecute, [Tool.Process.Executable]); Tool.ProcessStopped; exit; end; if not DirectoryExistsUTF8(ChompPathDelim(Tool.Process.CurrentDirectory)) then begin Tool.ErrorMessage:=Format(lisMissingDirectory, [Tool.Process. CurrentDirectory]); Tool.ProcessStopped; exit; end; // Under Unix TProcess uses fpFork, which means the current thread is // duplicated. One is the old thread and one runs fpExecve. // If fpExecve runs, then it will not return. // If fpExecve fails it returns via an exception and this thread runs twice. ok:=false; try {$IFDEF VerboseExtToolThread} DebuglnThreadLog(['TExternalToolThread.Execute ',Title,' execute ...']); {$ENDIF} // now execute Tool.Process.PipeBufferSize:=Max(Tool.Process.PipeBufferSize,64*1024); Tool.Process.Execute; {$IFDEF VerboseExtToolThread} DebuglnThreadLog(['TExternalToolThread.Execute ',Title,' executing ...']); {$ENDIF} ok:=true; except on E: Exception do begin // BEWARE: we are now either in the normal thread or in the failed forked thread {$IFDEF VerboseExtToolThread} DebuglnThreadLog(['TExternalToolThread.Execute ',Title,' execute failed: ',E.Message]); {$ENDIF} if Tool.ErrorMessage='' then Tool.ErrorMessage:=Format(lisUnableToExecute, [E.Message]); end; end; // BEWARE: we are now either in the normal thread or in the failed forked thread if not ok then begin Tool.ProcessStopped; exit; end; // we are now in the normal thread if Tool.Stage>=etsStopped then exit; {$IFDEF VerboseExtToolThread} DebuglnThreadLog(['TExternalToolThread.Execute ',Title,' ProcessRunning ...']); {$ENDIF} Tool.ProcessRunning; if Tool.Stage>=etsStopped then exit; {$IFDEF VerboseExtToolThread} DebuglnThreadLog(['TExternalToolThread.Execute ',Title,' reading ...']); {$ENDIF} OutputLine:=''; StdErrLine:=''; LastUpdate:=GetTickCount64; IdleCount:=0; while (Tool<>nil) and (Tool.Stage=etsRunning) do begin if Tool.ReadStdOutBeforeErr then begin HasOutput:=ReadInputPipe(Tool.Process.Output,OutputLine,false) or ReadInputPipe(Tool.Process.Stderr,StdErrLine,true); end else begin HasOutput:=ReadInputPipe(Tool.Process.Stderr,StdErrLine,true) or ReadInputPipe(Tool.Process.Output,OutputLine,false); end; if HasOutput then IdleCount:=0 else begin // no more pending output if not Tool.Process.Running then break; end; if (fLines.Count>0) and (Abs(int64(GetTickCount64)-LastUpdate)>UpdateTimeDiff) then begin {$IFDEF VerboseExtToolThread} DebuglnThreadLog(['TExternalToolThread.Execute ',Title,' ',TimeToStr(Now),' ',IntToStr(GetTickCount64),' AddOutputLines ...']); {$ENDIF} Tool.AddOutputLines(fLines); {$IFDEF VerboseExtToolThread} DebuglnThreadLog(['TExternalToolThread.Execute ',Title,' AddOutputLines ok']); {$ENDIF} fLines.Clear; LastUpdate:=GetTickCount64; end; if (not HasOutput) then begin // no more pending output and process is still running // => tool needs some time if Tool.MaxIdleInMS>0 then begin if IdleCount>Tool.MaxIdleInMS then break; Sleep(20); inc(IdleCount,20); end else begin Sleep(50); end; end; end; {$IFDEF VerboseExtToolThread} DebuglnThreadLog(['TExternalToolThread.Execute ',Title,' end reading']); {$ENDIF} // add rest of output if (OutputLine<>'') then fLines.Add(OutputLine); if (StdErrLine<>'') then fLines.Add(StdErrLine); if (Tool<>nil) and (fLines.Count>0) then begin {$IFDEF VerboseExtToolThread} DebuglnThreadLog(['TExternalToolThread.Execute ',Title,' final AddOutputLines ...']); {$ENDIF} Tool.AddOutputLines(fLines); {$IFDEF VerboseExtToolThread} DebuglnThreadLog(['TExternalToolThread.Execute ',Title,' final AddOutputLines ok']); {$ENDIF} fLines.Clear; end; try if Tool.Stage>=etsStopped then begin {$IFDEF VerboseExtToolThread} DebuglnThreadLog(['TExternalToolThread.Execute ',Title,' not reading exit status, because already stopped']); {$ENDIF} exit; end; {$IFDEF VerboseExtToolThread} DebuglnThreadLog(['TExternalToolThread.Execute ',Title,' reading exit status ...']); {$ENDIF} if (Tool.MaxIdleInMS<1) or (IdleCount0 then DebuglnThreadLog(['TExternalToolThread.Execute ',Title,' exit status=',Tool.ExitStatus,' ExitCode=',Tool.ExitCode]); {$ENDIF} except Tool.ErrorMessage:=lisUnableToReadProcessExitStatus; end; except on E: Exception do begin {$IFDEF VerboseExtToolThread} DebuglnThreadLog(['TExternalToolThread.Execute ',Title,' run: ',E.Message]); {$ENDIF} if (Tool<>nil) and (Tool.ErrorMessage='') then begin Tool.ErrorMessage:=E.Message; ErrMsg:=GetExceptionStackTrace; {$IFDEF VerboseExtToolErrors} DebuglnThreadLog(ErrMsg); {$ENDIF} Tool.ErrorMessage:=E.Message+LineEnding+ErrMsg; end; end; end; finally {$IFDEF VerboseExtToolThread} if fLines<>nil then DebuglnThreadLog(['TExternalToolThread.Execute ',Title,' cleaning up']); {$ENDIF} // clean up try FreeAndNil(fLines); except on E: Exception do begin {$IFDEF VerboseExtToolThread} DebuglnThreadLog(['TExternalToolThread.Execute adding pending messages: ',E.Message]); {$ENDIF} if Tool<>nil then Tool.ErrorMessage:=Format(lisFreeingBufferLines, [E.Message]); end; end; end; if Tool.Stage>=etsStopped then begin {$IFDEF VerboseExtToolThread} DebuglnThreadLog(['TExternalToolThread.Execute ',Title,' not cleaning up']); {$ENDIF} exit; end; {$IFDEF VerboseExtToolThread} DebuglnThreadLog(['TExternalToolThread.Execute ',Title,' ProcessStopped ...']); {$ENDIF} Tool.ProcessStopped; {$IFDEF VerboseExtToolThread} DebuglnThreadLog(['TExternalToolThread.Execute ',Title,' Thread END']); {$ENDIF} end; procedure TExternalToolThread.DebuglnThreadLog(const Args: array of string); var s, aFilename: String; fs: TFileStream; i: Integer; begin s:='['+IntToStr(PtrUint(GetThreadID))+'] '; for i:=low(Args) to High(Args) do s:=s+Args[i]; s:=s+sLineBreak; aFilename:='TExternalToolThread-DebuglnThreadLog.txt'; if FileExists(aFilename) then fs:=TFileStream.Create(aFilename,fmOpenWrite or fmShareDenyNone) else fs:=TFileStream.Create(aFilename,fmCreate or fmShareDenyNone); try fs.Seek(0,soEnd); fs.Write(s[1],length(s)); finally fs.Free; end; end; destructor TExternalToolThread.Destroy; begin Tool:=nil; inherited Destroy; end; end.