lazarus/ide/exttools.pas

1846 lines
53 KiB
ObjectPascal

{
***************************************************************************
* *
* 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 <http://www.gnu.org/copyleft/gpl.html>. 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 OldMsgCount<WorkerMessages.Count then begin
for i:=0 to ParserCount-1 do begin
Parser:=Parsers[i];
Parser.NeedSynchronize:=false;
Parser.NeedAfterSync:=false;
{$IFDEF VerboseExtToolAddOutputLines}
DebuglnThreadLog(['TExternalTool.AddOutputLines ',DbgSName(Parser),' IMPROVE after ReadLine ...']);
{$ENDIF}
Parser.ImproveMessages(etpspAfterReadLine);
if Parser.NeedSynchronize then
NeedSynchronize:=true;
end;
end;
finally
LeaveCriticalSection;
end;
// let all parsers improve the new messages in main thread
if NeedSynchronize then begin
{$IFDEF VerboseExtToolAddOutputLines}
DebuglnThreadLog(['TExternalTool.AddOutputLines SynchronizedImproveMessages ...']);
{$ENDIF}
if Thread<>nil 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 OldMsgCount<WorkerMessages.Count then begin
for i:=0 to ViewCount-1 do begin
{$IFDEF VerboseExtToolAddOutputLines}
DebuglnThreadLog(['TExternalTool.AddOutputLines ',DbgSName(Views[i]),' "',Views[i].Caption,'" ProcessNewMessages ...']);
{$ENDIF}
Views[i].ProcessNewMessages(Thread);
end;
end;
finally
LeaveCriticalSection;
end;
// notify main thread handlers for new output
// Note: The IDE itself does not set such a handler
if {$IFDEF VerboseExtToolAddOutputLines}true{$ELSE}FHandlers[ethNewOutput].Count>0{$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 Stage<etsStarting then
FStage:=etsStopped
else if Stage<=etsRunning then
FStage:=etsWaitingForStop;
finally
LeaveCriticalSection;
end;
if NeedProcTerminate and (Process<>nil) 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)<ord(etsStopped) then exit;
if ExecBefore.ErrorMessage<>'' 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 RunningCount<MaxProcessCount do begin
Tool:=FindNextToolToStart;
if Tool=nil then
break;
Tool.DoStart;
end;
FreeFinishedThreads;
end;
function TExternalTools.FindNextToolToStart: TExternalTool;
var
Tool: TExternalTool;
CurLoad: Int64;
Load: Int64;
i: Integer;
begin
Result:=nil;
Load:=0;
for i:=0 to Count-1 do begin
Tool:=TExternalTool(Items[i]);
//debugln(['TExternalTools.FindNextToolToExec ',Tool.Title,' ',Tool.CanStart]);
if not Tool.CanStart then continue;
CurLoad:=Tool.GetLongestEstimatedLoad;
if (Result<>nil) 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 (i<Count) and (Buf[i+1] in [#10,#13]) and (Buf[i]<>Buf[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 (IdleCount<Tool.MaxIdleInMS) then
begin
Tool.ExitStatus:=Tool.Process.ExitStatus;
Tool.ExitCode:=Tool.Process.ExitCode;
end;
{$IFDEF VerboseExtToolThread}
if Tool.ExitStatus<>0 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.