lazarus/components/lazdebuggers/cmdlinedebuggerbase/debugprocess.pas
martin 3f2cdc7c27 Debugger: remove some active logging
git-svn-id: trunk@60130 -
2019-01-21 00:18:23 +00:00

594 lines
17 KiB
ObjectPascal

{
This unit contains the Commandline debugger class for external commandline
debuggers.
***************************************************************************
* *
* 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. *
* *
***************************************************************************
}
unit DebugProcess;
{$mode objfpc}{$H+}
{ $DEFINE DBG_VERBOSE}
{ $DEFINE DBG_VERBOSE_FULL_DATA}
{$IFDEF MSWindows} // optional gtk
{$DEFINE NATIVE_ASYNC_PROCESS}
{$ELSE}
{$UNDEF NATIVE_ASYNC_PROCESS}
{$ENDIF}
interface
uses
Classes, sysutils, AsyncProcess, LCLIntf, InterfaceBase, process,
Pipes, LazLoggerBase, UTF8Process;
type
TDebugProcessNotification = procedure(Sender: TObject; ALine: String) of object;
{ TDebugProcessReadThread }
{$IFnDEF NATIVE_ASYNC_PROCESS}
TDebugProcessReadThread = class(TThread)
private
FAsyncLoopWaitEvent: PRTLEvent;
protected
FStream: TInputPipeStream;
FOnDataAvail: TThreadMethod;
FOnPipeError: TThreadMethod;
procedure Execute; override;
public
constructor Create(CreateSuspended: Boolean; const StackSize: SizeUInt =
DefaultStackSize);
destructor Destroy; override;
property AsyncLoopWaitEvent: PRTLEvent read FAsyncLoopWaitEvent;
end;
{$ENDIF}
{ TDebugAsyncProcess }
TDebugAsyncProcess = class(TProcessUTF8) // TAsyncProcess
private
{$IFdef NATIVE_ASYNC_PROCESS}
FPipeHandler: PPipeEventHandler;
FProcessHandler: PProcessEventHandler;
{$ELSE}
FReadThread: TDebugProcessReadThread;
{$ENDIF}
FOnReadData: TNotifyEvent;
FOnTerminate: TNotifyEvent;
{$ifNdef NATIVE_ASYNC_PROCESS}
procedure ThreadDataAvail;
procedure ThreadPipeError;
procedure TerminataThread;
{$ENDIF}
procedure FinishedReadingOutput;
protected
procedure HandlePipeInput(AData: PtrInt; AReasons: TPipeReasons);
procedure HandleProcessTermination(AData: PtrInt; AReason: TChildExitReason; AInfo: dword);
procedure UnhookPipeHandle;
procedure UnhookProcessHandle;
public
procedure Execute; override;
destructor Destroy; override;
function Terminate(AExitCode: Integer): Boolean; override;
published
property OnReadData: TNotifyEvent read FOnReadData write FOnReadData;// You must read all the data in this event. Otherwise it is called again.
property OnTerminate: TNotifyEvent read FOnTerminate write FOnTerminate;
end;
{ TDebugProcess }
TDebugProcess = class
private const
DBG_STREAM_READ_LEN = 8192;
private
FExternalDebugger: String;
FDbgProcess: TDebugAsyncProcess;
FOnLineReceived: TDebugProcessNotification;
FOnBeginLinesReceived: TNotifyEvent;
FOnEndLinesReceived: TNotifyEvent;
FOnLineSent: TDebugProcessNotification;
FOnSendError: TDebugProcessNotification;
FOnTerminate: TNotifyEvent;
FTmpBuffer: String;
FOutputBuf: String;
FLockReadData: Boolean;
procedure DoReadData(Sender: TObject);
procedure DoTerminate(Sender: TObject);
function GetDbgProcess: TProcessUTF8;
function HandleHasData(const AHandle: Integer): Boolean;
protected
function GetDebugProcessRunning: Boolean;
public
constructor Create(const AExternalDebugger: String);
destructor Destroy; override;
function CreateDebugProcess(const AOptions: String; AnEnvironment: TStrings): Boolean;
procedure StopDebugProcess;
procedure SendCmdLn(const ACommand: String); overload;
procedure SendCmdLn(const ACommand: String; Values: array of const); overload;
public
property DebugProcess: TProcessUTF8 read GetDbgProcess;
property DebugProcessRunning: Boolean read GetDebugProcessRunning;
property OnLineReceived: TDebugProcessNotification read FOnLineReceived write FOnLineReceived;
property OnBeginLinesReceived: TNotifyEvent read FOnBeginLinesReceived write FOnBeginLinesReceived;
property OnEndLinesReceived: TNotifyEvent read FOnEndLinesReceived write FOnEndLinesReceived;
property OnLineSent: TDebugProcessNotification read FOnLineSent write FOnLineSent;
property OnSendError: TDebugProcessNotification read FOnSendError write FOnSendError;
// property OnTimeOut: TDebugProcessNotification read FOnTimeOut write FOnTimeOut;
property OnTerminate: TNotifyEvent read FOnTerminate write FOnTerminate;
end;
implementation
uses
{$IFdef MSWindows} Windows {$ENDIF}
{$IFDEF UNIX} Unix, BaseUnix {$ENDIF}
;
var
DBG_CMD_ECHO, DBG_CMD_ECHO_FULL, DBG_VERBOSE, DBG_WARNINGS: PLazLoggerLogGroup;
{ TDebugProcessReadThread }
{$IFnDEF NATIVE_ASYNC_PROCESS}
procedure TDebugProcessReadThread.Execute;
var
R: Integer;
FDS: TFDSet;
begin
while (not Terminated) and (FStream.Handle >= 0) do begin
FpFD_ZERO(FDS);
FpFD_Set(FStream.Handle, FDS);
// R = -1 on error, 0 on timeout, >0 on success and is number of handles
// FDS is changed, and indicates what descriptors have changed
R := FpSelect(FStream.Handle + 1, @FDS, nil, nil, 50);
if Terminated then
break;
if r < 0 then begin
DebugLn('TTTTT pipe err');
Queue(FOnPipeError);
exit;
end;
if (R > 0) and (FpFD_ISSET(FStream.Handle,FDS)=1) then begin
DebugLn('TTTTT data avail');
Queue(FOnDataAvail);
RTLeventWaitFor(FAsyncLoopWaitEvent);
DebugLn('TTTTT data avail continue');
end;
end;
DebugLn(['TTTTT loop end ', Terminated]);
RemoveQueuedEvents(Self, FOnDataAvail);
RemoveQueuedEvents(Self, FOnPipeError);
end;
constructor TDebugProcessReadThread.Create(CreateSuspended: Boolean;
const StackSize: SizeUInt);
begin
FAsyncLoopWaitEvent := RTLEventCreate;
inherited;
end;
destructor TDebugProcessReadThread.Destroy;
begin
inherited Destroy;
RTLeventdestroy(FAsyncLoopWaitEvent);
end;
{$ENDIF}
{ TDebugAsyncProcess }
procedure TDebugAsyncProcess.FinishedReadingOutput;
begin
{$ifNdef NATIVE_ASYNC_PROCESS}
// Either finished reading, or TThread.Terminate was called
if FReadThread <> nil then
RTLeventSetEvent(FReadThread.AsyncLoopWaitEvent);
{$ENDIF}
end;
{$ifNdef NATIVE_ASYNC_PROCESS}
procedure TDebugAsyncProcess.ThreadDataAvail;
begin
if not Running then begin
//HandlePipeInput(0, [prBroken]);
HandleProcessTermination(0, cerExit, 0);
exit;
end;
HandlePipeInput(0, [prDataAvailable]);
// SELF may have been destroyed, during read or handle-termination
end;
procedure TDebugAsyncProcess.ThreadPipeError;
begin
DebugLn(['got pipe err / is running ', Running]);
Terminate(0);
HandleProcessTermination(0, cerExit, 0);
end;
procedure TDebugAsyncProcess.TerminataThread;
begin
FReadThread.Terminate;
FinishedReadingOutput;
TThread.RemoveQueuedEvents(FReadThread, @ThreadDataAvail);
TThread.RemoveQueuedEvents(FReadThread, @ThreadPipeError);
end;
{$ENDIF}
procedure TDebugAsyncProcess.HandlePipeInput(AData: PtrInt;
AReasons: TPipeReasons);
begin
if prBroken in AReasons then
UnhookPipeHandle;
if prDataAvailable in AReasons then
if FOnReadData <> nil then
FOnReadData(Self);
end;
procedure TDebugAsyncProcess.HandleProcessTermination(AData: PtrInt;
AReason: TChildExitReason; AInfo: dword);
begin
DebugLn('HandleProcessTermination');
UnhookProcessHandle;
UnhookPipeHandle;
if FOnTerminate <> nil then
FOnTerminate(Self);
end;
procedure TDebugAsyncProcess.UnhookPipeHandle;
begin
{$IFdef NATIVE_ASYNC_PROCESS}
if FPipeHandler <> nil then
RemovePipeEventHandler(FPipeHandler);
{$ELSE}
if FReadThread <> nil then begin
TerminataThread;
end;
{$ENDIF}
end;
procedure TDebugAsyncProcess.UnhookProcessHandle;
begin
{$IFdef NATIVE_ASYNC_PROCESS}
if FProcessHandler <> nil then
RemoveProcessEventHandler(FProcessHandler);
{$ELSE} // should be enough in UnhookPipeHandle;
if FReadThread <> nil then begin
TerminataThread;
end;
{$ENDIF}
end;
procedure TDebugAsyncProcess.Execute;
begin
inherited Execute;
{$IFdef NATIVE_ASYNC_PROCESS}
if poUsePipes in Options then
FPipeHandler := AddPipeEventHandler(Output.Handle, @HandlePipeInput, 0);
FProcessHandler := AddProcessEventHandler(ProcessHandle, @HandleProcessTermination, 0);
{$ELSE}
if FReadThread = nil then
FReadThread := TDebugProcessReadThread.Create(false);
FReadThread.FStream := Output;
FReadThread.FOnDataAvail := @ThreadDataAvail;
FReadThread.FOnPipeError := @ThreadPipeError;
FReadThread.Start;
{$ENDIF}
end;
destructor TDebugAsyncProcess.Destroy;
begin
{$IFdef NATIVE_ASYNC_PROCESS}
UnhookProcessHandle;
UnhookPipeHandle;
{$ELSE}
if FReadThread <> nil then begin
TerminataThread;
FReadThread.WaitFor;
debugln(['DESTROY thread destroying']);
FreeAndNil(FReadThread);
debugln(['DESTROY thread destroyed']);
end;
{$ENDIF}
inherited;
end;
function TDebugAsyncProcess.Terminate(AExitCode: Integer): Boolean;
begin
{$ifdef NATIVE_ASYNC_PROCESS}
UnhookProcessHandle;
UnhookPipeHandle;
{$ELSE}
if FReadThread <> nil then begin
TerminataThread;
FReadThread.WaitFor;
debugln(['DESTROY thread destroying']);
FreeAndNil(FReadThread);
debugln(['DESTROY thread destroyed']);
end;
{$ENDIF}
Result := inherited Terminate(AExitCode);
end;
{ TDebugProcess }
procedure TDebugProcess.DoReadData(Sender: TObject);
function ReadData(const AStream: TInputPipeStream; var ABuffer: String): Integer;
var
c: LongInt;
begin
Result := 0;
c := AStream.Read(FTmpBuffer[1], DBG_STREAM_READ_LEN);
while c > 0 do begin
SetLength(ABuffer, Length(ABuffer)+c);
Move(FTmpBuffer[1], ABuffer[1 + Result], c);
Result := Result + c;
if (c = DBG_STREAM_READ_LEN) and HandleHasData(AStream.Handle) then begin
c := AStream.Read(FTmpBuffer[1], DBG_STREAM_READ_LEN);
end
else
c := 0;
end;
end;
function LineEndPos(const s: string; out LineEndLen: integer): integer;
var
n, idx: Integer;
begin
LineEndLen := 0;
Result := pos(#10, s);
n := pos(#13, s);
if (n > 0) and (n < Result) then
Result := n;
if Result = 0 then exit;
LineEndLen := 1;
if Result < Length(s) then begin
if (s[Result+1] in [#10,#13]) and (s[Result+1] <> s[Result]) then
LineEndLen := 2;
end;
end;
var
LineEndIdx, LineEndLen: Integer;
Line: String;
begin
if not DebugProcessRunning then begin
StopDebugProcess;
exit;
end;
if (FDbgProcess.Output <> nil) then
ReadData(FDbgProcess.Output, Line);
FOutputBuf := FOutputBuf + Line;
FDbgProcess.FinishedReadingOutput; // Allow new reads, while we are processing
if FLockReadData or (FOutputBuf = '') then
exit;
try
FLockReadData := True;
if FOnBeginLinesReceived <> nil then // use to UnlockRelease
FOnBeginLinesReceived(Self);
LineEndIdx := LineEndPos(FOutputBuf, LineEndLen);
while (LineEndIdx > 0) do begin
Dec(LineEndIdx);
Line := Copy(FOutputBuf, 1, LineEndIdx);
Delete(FOutputBuf, 1, LineEndIdx + LineEndLen);
if ((DBG_CMD_ECHO_FULL <> nil) and (DBG_CMD_ECHO_FULL^. Enabled))
then debugln(DBG_CMD_ECHO_FULL, '<< << TCmdLineDebugger.ReadLn "',Line,'"')
else if (length(Line) < 300)
then debugln(DBG_CMD_ECHO, '<< << TCmdLineDebugger.ReadLn "',Line,'"')
else debugln(DBG_CMD_ECHO, ['<< << TCmdLineDebugger.ReadLn "',copy(Line, 1, 200), '" ..(',length(Line)-300,').. "',copy(Line, length(Line)-99, 100),'"']);
if FOnLineReceived <> nil then
FOnLineReceived(Self, Line);
LineEndIdx := LineEndPos(FOutputBuf, LineEndLen);
end;
finally
FLockReadData := False;
if FOnEndLinesReceived <> nil then // use to LockRelease
FOnEndLinesReceived(Self);
// Debugger and Self may get destroyed at this point
end;
end;
procedure TDebugProcess.DoTerminate(Sender: TObject);
begin
if FOnTerminate <> nil then
FOnTerminate(Self);
end;
function TDebugProcess.GetDbgProcess: TProcessUTF8;
begin
Result := FDbgProcess;
end;
function TDebugProcess.HandleHasData(const AHandle: Integer): Boolean;
{$IFDEF UNIX}
var
R: Integer;
FDS: TFDSet;
begin
Result := False;
if AHandle < 0 then
exit;
FpFD_ZERO(FDS);
FpFD_Set(AHandle, FDS);
// R = -1 on error, 0 on timeout, >0 on success and is number of handles
// FDS is changed, and indicates what descriptors have changed
R := FpSelect(AHandle + 1, @FDS, nil, nil, 1);
Result := (R > 0) and (FpFD_ISSET(AHandle,FDS)=1);
end;
{$ELSE linux}
{$IFdef MSWindows}
var
TotalBytesAvailable: dword;
R: LongBool;
begin
R := Windows.PeekNamedPipe(AHandle, nil, 0, nil, @TotalBytesAvailable, nil);
if not R then begin
// PeekNamedPipe failed
DebugLn(DBG_WARNINGS, ['PeekNamedPipe failed, GetLastError is ', GetLastError]);
Exit;
end;
Result := TotalBytesAvailable > 0;
end;
{$ELSE win32}
begin
DebugLn('ToDo: implement WaitForHandles for this OS');
Result := 0;
end;
{$ENDIF win32}
{$ENDIF linux}
function TDebugProcess.GetDebugProcessRunning: Boolean;
begin
Result := (FDbgProcess <> nil) and FDbgProcess.Running;
end;
constructor TDebugProcess.Create(const AExternalDebugger: String);
begin
FDbgProcess := nil;
FExternalDebugger := AExternalDebugger;
SetLength(FTmpBuffer, DBG_STREAM_READ_LEN);
inherited Create;
end;
destructor TDebugProcess.Destroy;
begin
if DebugProcessRunning then
StopDebugProcess; // calls FDbgProcess.Release;
try
FDbgProcess.Destroy;
except
on E: Exception do DebugLn(DBG_WARNINGS, 'Exception while freeing debugger: ', E.Message);
end;
inherited Destroy;
end;
function TDebugProcess.CreateDebugProcess(const AOptions: String;
AnEnvironment: TStrings): Boolean;
begin
Result := False;
if FDbgProcess = nil
then begin
FDbgProcess := TDebugAsyncProcess.Create(nil);
try
FDbgProcess.Options:= [poUsePipes, poNoConsole, poStdErrToOutPut, poNewProcessGroup];
{$if defined(windows) and not defined(wince)}
// under win9x and winMe should be created with console,
// otherwise no break can be send.
if Win32MajorVersion <= 4 then
FDbgProcess.Options:= [poUsePipes, poNewConsole, poStdErrToOutPut, poNewProcessGroup];
{$endif windows}
FDbgProcess.ShowWindow := swoNone;
except
FreeAndNil(FDbgProcess);
end;
end;
if FDbgProcess = nil then exit;
FDbgProcess.ParseCmdLine(FExternalDebugger + ' ' + AOptions);
FDbgProcess.Environment:= AnEnvironment;
FDbgProcess.OnReadData := @DoReadData;
FDbgProcess.OnTerminate := @DoTerminate;
if not FDbgProcess.Running
then begin
try
FDbgProcess.Execute;
DebugLn(DBG_VERBOSE, '[TDebugProcess] Debug PID: ', IntToStr(FDbgProcess.Handle));
Result := FDbgProcess.Running;
except
on E: Exception do begin
FOutputBuf := E.Message;
DebugLn(DBG_WARNINGS, 'Exception while executing debugger: ', FOutputBuf);
end;
end;
end;
end;
procedure TDebugProcess.StopDebugProcess;
begin
debugln(['TDebugProcess.StopDebugProcess FDbgProcess = nil ',FDbgProcess = nil]);
if FDbgProcess = nil then exit;
FDbgProcess.Terminate(0);
end;
procedure TDebugProcess.SendCmdLn(const ACommand: String);
const
LE: string = LineEnding;
begin
if (DBG_CMD_ECHO_FULL <> nil) and (DBG_CMD_ECHO_FULL^.Enabled)
then debugln(DBG_CMD_ECHO_FULL, '>> >> TDebugProcess.SendCmdLn "',ACommand,'"')
else debugln(DBG_CMD_ECHO, '>> >> TDebugProcess.SendCmdLn "',ACommand,'"');
if DebugProcessRunning
then begin
if FOnLineSent <> nil then
FOnLineSent(Self, ACommand);
if ACommand <> ''
then FDbgProcess.Input.Write(ACommand[1], Length(ACommand));
FDbgProcess.Input.Write(LE[1], Length(LE));
end
else begin
DebugLn(DBG_WARNINGS, '[TDebugProcess.SendCmdLn] Unable to send <', ACommand, '>. No process running.');
if FOnSendError <> nil then
FOnSendError(Self, ACommand);
end;
end;
procedure TDebugProcess.SendCmdLn(const ACommand: String;
Values: array of const);
begin
SendCmdLn(Format(ACommand, Values));
end;
initialization
DBG_CMD_ECHO := DebugLogger.FindOrRegisterLogGroup('DBG_CMD_ECHO' {$IF defined(DBG_VERBOSE) or defined(DBG_CMD_ECHO)} , True {$ENDIF} );
DBG_CMD_ECHO_FULL := DebugLogger.FindOrRegisterLogGroup('DBG_CMD_ECHO_FULL' {$IF defined(DBG_VERBOSE_FULL_DATA) or defined(DBG_CMD_ECHO_FULL)} , True {$ENDIF} );
DBG_VERBOSE := DebugLogger.FindOrRegisterLogGroup('DBG_VERBOSE' {$IFDEF DBG_VERBOSE} , True {$ENDIF} );
DBG_WARNINGS := DebugLogger.FindOrRegisterLogGroup('DBG_WARNINGS' {$IFDEF DBG_WARNINGS} , True {$ENDIF} );
end.