lazarus/components/fpdebug/app/fpdserver/debugthread.pas
2024-01-14 19:29:14 +01:00

680 lines
20 KiB
ObjectPascal

unit debugthread;
{$mode objfpc}{$H+}
{$INTERFACES CORBA}
interface
uses
Classes,
SysUtils, fgl,
FPDbgController,
FpDbgDwarfDataClasses,
FpdMemoryTools,
DbgIntfBaseTypes,
DbgIntfDebuggerBase,
lazCollections,
syncobjs,
LazFglHash,
fpjson,
FpDbgClasses, LazDebuggerIntf;
type
// The debug-thread sends three different kind of messages to it's listeners
TFpDebugEventType = (
etEvent, // Messages that are send by the debugger. (debuggee has been started, pauzed, stopped, etc.)
etLog, // Log-messages send by the debugger. (Fpdebug also uses log-messages to inform users of some
// events (the dllInfo-log messages)
etNotification // Messages from the debug-thread itself. Including new or lost connections and commands that
// are queued or executed.
);
// The different kinds of etNotifications
TFpDebugNotificationType = (
ntNewConnection,
ntLostConnection,
ntInvalidCommand,
ntConnectionProblem,
ntListenerMessage,
ntReceivedCommand,
ntExecutedCommand,
ntFailedCommand
);
TFpDebugEventCallStackEntry = record
AnAddress: TDBGPtr;
FrameAdress: TDBGPtr;
SourceFile: string;
FunctionName: string;
Line: integer;
end;
TFpDebugEventWatchEntry = record
TextValue: string;
Expression: string;
NumValue: TDBGPtr;
Size: byte;
end;
TFpDebugEventCallStackEntryArray = array of TFpDebugEventCallStackEntry;
TFpDebugEventDisassemblerEntryArray = array of TDisassemblerEntry;
TFpDebugEventWatchEntryArray = array of TFpDebugEventWatchEntry;
TFPDLogLevel = (dllDebug, dllInfo, dllError);
TOnLog = procedure(const AString: string; const ALogLevel: TFPDLogLevel) of object;
// This record is used to pass debugging-events. Not every field is applicable for each type of event.
TFpDebugEvent = record
SendByConnectionIdentifier: integer;
EventType: TFpDebugEventType;
NotificationType: TFpDebugNotificationType;
Message: string;
EventName: string;
LogLevel: TFPDLogLevel;
InstructionPointerRegValue: TDBGPtr;
AnUID: variant;
BreakpointServerIdr: Integer;
LocationRec: TDBGLocationRec;
Validity: TDebuggerDataState;
Addr1: TDBGPtr;
Addr2: TDBGPtr;
Addr3: TDBGPtr;
StackEntryArray: TFpDebugEventCallStackEntryArray;
DisassemblerEntryArray: TFpDebugEventDisassemblerEntryArray;
WatchEntryArray: TFpDebugEventWatchEntryArray;
end;
// Each listener should implement this interface.
IFpDebugListener = interface ['{2230763A-672E-4EC1-941D-6B8814D789C8}']
// This procedure is called by the debugthread when there is a message for the listener.
// Not that this procedure will be called from within the debug-thread, and should not take too much
// resources, or ot will slow down the debugging.
procedure SendEvent(AnEvent: TFpDebugEvent);
// Gives more information about the origin of the listener.
function GetOrigin: string;
end;
TFpDebugThread = class;
{ TFpServerDbgController }
TFpServerDbgController = class(TDbgController)
private type
TBreakPointIdMap = specialize TFPGMap<Integer, TFpDbgBreakpoint>;
function DoBreakPointCompare(Key1, Key2: Pointer): Integer;
private
FBreakPointIdCnt: Integer;
FBreakPointIdMap: TBreakPointIdMap;
public
constructor Create(AMemManager: TFpDbgMemManager; AMemModel: TFpDbgMemModel); override;
destructor Destroy; override;
function AddInternalBreakPointToId(ABrkPoint: TFpDbgBreakpoint): Integer;
function GetInternalBreakPointFromId(AnId: Integer): TFpDbgBreakpoint;
function GetIdFromInternalBreakPoint(ABrkPoint: TFpDbgBreakpoint): Integer;
procedure RemoveInternalBreakPoint(AnId: Integer);
//procedure RemoveInternalBreakPoint(ABrkPoint: TFpDbgBreakpoint);
procedure ClearInternalBreakPoint;
end;
{ TFpDebugThreadCommand }
// The base class for all commands that can be send to the debug-thread.
TFpDebugThreadCommand = class
private
FOnLog: TOnLog;
protected
FListenerIdentifier: integer;
FUID: variant;
procedure Log(const AString: string; const ALogLevel: TFPDLogLevel);
public
constructor Create(AListenerIdentifier: integer; AnUID: variant; AOnLog: TOnLog); virtual;
// Descendents may override this procedure to add additionol information to the event that will
// be send to all listeners when a command has been received succesfully
procedure ComposeReceiveEvent(var AnEvent: TFpDebugEvent); virtual;
// As above, for commands that has been executed successfully
procedure ComposeSuccessEvent(var AnEvent: TFpDebugEvent); virtual;
// As above, for commands that has failed to execute.
procedure ComposeFailureEvent(var AnEvent: TFpDebugEvent); virtual;
// Descendents have to override this function to implement the actual command. This function is called from within
// the controller's debug loop. (This means it is only executed when the debuggee is paused or stopped)
// Should return true on success, false on a failure. Set DoProcessLoop to true when the debuggee should continue,
// make it false if the debuggee should stay in a paused state.
function Execute(AController: TFpServerDbgController; out DoProcessLoop: boolean): boolean; virtual; abstract;
// This method is called before the command is queued for execution in the controller's debug loop. This
// can happen in any thread. If DoQueueCommand is true, the result is ignored or else a success-event is
// send if the result is true, a failure if the result is false.
function PreExecute(AController: TFpServerDbgController; out DoQueueCommand: boolean): boolean; virtual;
// The name that is used to identify the command
class function TextName: string; virtual; abstract;
// The identifier of the Listener that has send this command
property ListenerIdentifier: integer read FListenerIdentifier;
end;
TFpDebugThreadCommandClass = class of TFpDebugThreadCommand;
TFpDebugThreadCommandQueue = specialize TLazThreadedQueue<TFpDebugThreadCommand>;
{ TFpDebugThread }
TFpDebugThread = class(TThread)
private
FCommandQueue: TFpDebugThreadCommandQueue;
FController: TFpServerDbgController;
FListenerList: TThreadList;
FMemConverter: TFpDbgMemConvertorLittleEndian;
FMemReader: TDbgMemReader;
FMemModel: TFpDbgMemModel;
FMemManager: TFpDbgMemManager;
FConsoleOutputThread: TThread;
procedure FreeConsoleOutputThread;
protected
// Handlers for the FController-events
procedure FControllerHitBreakpointEvent(var continue: boolean; const Breakpoint: TFpDbgBreakpoint;
AnEventType: TFPDEvent; AMoreHitEventsPending: Boolean);
procedure FControllerProcessExitEvent(ExitCode: DWord);
procedure FControllerCreateProcessEvent(var continue: boolean);
procedure FControllerDebugInfoLoaded(Sender: TObject);
// Main debug thread-loop
procedure Execute; override;
// Send an event to all listeners
procedure SendEvent(ADebugEvent: TFpDebugEvent);
procedure ClearEvent(var AnEvent: TFpDebugEvent);
public
constructor Create;
destructor Destroy; override;
class function Instance: TFpDebugThread;
// Sends a command to the command-queue, the command-queue takes ownership of the command.
procedure QueueCommand(ACommand: TFpDebugThreadCommand);
// Procedures to send notifications and log-messages to the listeners
procedure SendNotification(AConnectionIdentifier: integer; ANotificationType: TFpDebugNotificationType; AnUID: variant; AMessage, ACommand: string);
procedure SendNotification(AConnectionIdentifier: integer; ANotificationType: TFpDebugNotificationType; AnUID: variant; AMessage, ACommand: string; Arg: array of const); overload;
procedure SendLogMessage(const AString: string; const ALogLevel: TFPDLogLevel);
// Methods to add and remove listeners
function AddListener(AFpDebugListener: IFpDebugListener): integer;
procedure RemoveListener(AFpDebugListener: IFpDebugListener);
end;
const
FpEventTypeNames: array[TFpDebugEventType] of string = (
'event',
'log',
'notification');
FpDebugNotificationTypeNames: array[TFpDebugNotificationType] of string = (
'NewConnection',
'LostConnection',
'InvalidCommand',
'ConnectionProblem',
'ListenerMessage',
'ReceivedCommand',
'ExecutedCommand',
'FailedCommand');
implementation
var
FFpDebugThread: TFpDebugThread;
type
{ TFpDbgMemReader }
TFpDbgMemReader = class(TDbgMemReader)
private
FDebugThread: TFpDebugThread;
protected
function GetDbgProcess: TDbgProcess; override;
public
constructor create(ADebugThread: TFpDebugThread);
end;
{ TFpWaitForConsoleOutputThread }
TFpWaitForConsoleOutputThread = class(TThread)
private
FDebugThread: TFpDebugThread;
public
constructor Create(ADebugThread: TFpDebugThread);
procedure Execute; override;
end;
{ TFpServerDbgController }
function TFpServerDbgController.DoBreakPointCompare(Key1, Key2: Pointer
): Integer;
begin
Result := PPointer(Key1)^ - PPointer(Key1)^;
end;
constructor TFpServerDbgController.Create(AMemManager: TFpDbgMemManager; AMemModel: TFpDbgMemModel);
begin
FBreakPointIdMap := TBreakPointIdMap.Create;
FBreakPointIdMap.OnDataPtrCompare := @DoBreakPointCompare;
inherited Create(AMemManager, MemModel);
end;
destructor TFpServerDbgController.Destroy;
begin
inherited Destroy;
FBreakPointIdMap.Free;
end;
function TFpServerDbgController.AddInternalBreakPointToId(
ABrkPoint: TFpDbgBreakpoint): Integer;
begin
inc(FBreakPointIdCnt);
Result := FBreakPointIdCnt;
FBreakPointIdMap.Add(Result, ABrkPoint);
end;
function TFpServerDbgController.GetInternalBreakPointFromId(AnId: Integer
): TFpDbgBreakpoint;
begin
if not FBreakPointIdMap.TryGetData(AnId, Result) then
Result := nil;
end;
function TFpServerDbgController.GetIdFromInternalBreakPoint(
ABrkPoint: TFpDbgBreakpoint): Integer;
begin
Result := FBreakPointIdMap.IndexOfData(ABrkPoint);
end;
procedure TFpServerDbgController.RemoveInternalBreakPoint(AnId: Integer);
begin
FBreakPointIdMap.Remove(AnId);
end;
procedure TFpServerDbgController.ClearInternalBreakPoint;
begin
FBreakPointIdMap.Clear;
end;
constructor TFpWaitForConsoleOutputThread.Create(ADebugThread: TFpDebugThread);
begin
Inherited create(false);
FDebugThread := ADebugThread;
end;
procedure TFpWaitForConsoleOutputThread.Execute;
var
res: integer;
AnEvent: TFpDebugEvent;
s: string;
begin
while not terminated do
begin
res := FDebugThread.FController.CurrentProcess.CheckForConsoleOutput(100);
if res<0 then
Terminate
else if res>0 then
begin
FDebugThread.ClearEvent(AnEvent);
AnEvent.EventType:=etEvent;
AnEvent.EventName:='ConsoleOutput';
s := FDebugThread.FController.CurrentProcess.GetConsoleOutput;
if s <> '' then
begin
AnEvent.Message:=s;
FDebugThread.SendEvent(AnEvent);
end;
end;
end;
end;
{ TFpDbgMemReader }
function TFpDbgMemReader.GetDbgProcess: TDbgProcess;
begin
result := FDebugThread.FController.CurrentProcess;
end;
constructor TFpDbgMemReader.create(ADebugThread: TFpDebugThread);
begin
Inherited Create;
FDebugThread:=ADebugThread;
end;
{ TFpDebugThreadCommand }
procedure TFpDebugThreadCommand.Log(const AString: string; const ALogLevel: TFPDLogLevel);
begin
if assigned(FOnLog) then
FOnLog(AString, ALogLevel);
end;
constructor TFpDebugThreadCommand.Create(AListenerIdentifier: integer;
AnUID: variant; AOnLog: TOnLog);
begin
FListenerIdentifier:=AListenerIdentifier;
FUID:=AnUID;
FOnLog:=AOnLog;
end;
procedure TFpDebugThreadCommand.ComposeReceiveEvent(var AnEvent: TFpDebugEvent);
begin
AnEvent.EventType:=etNotification;
AnEvent.NotificationType:=ntReceivedCommand;
AnEvent.SendByConnectionIdentifier:=ListenerIdentifier;
AnEvent.AnUID:=FUID;
AnEvent.EventName:=TextName;
AnEvent.Message:=Format('Received %s-command.',[TextName]);
end;
procedure TFpDebugThreadCommand.ComposeSuccessEvent(var AnEvent: TFpDebugEvent);
begin
AnEvent.EventType:=etNotification;
AnEvent.NotificationType:=ntExecutedCommand;
AnEvent.SendByConnectionIdentifier:=ListenerIdentifier;
AnEvent.AnUID:=FUID;
AnEvent.EventName:=TextName;
AnEvent.Message:=Format('%s-command executed succesfully.',[TextName]);
end;
procedure TFpDebugThreadCommand.ComposeFailureEvent(var AnEvent: TFpDebugEvent);
begin
AnEvent.EventType:=etNotification;
AnEvent.NotificationType:=ntFailedCommand;
AnEvent.SendByConnectionIdentifier:=ListenerIdentifier;
AnEvent.AnUID:=FUID;
AnEvent.EventName:=TextName;
AnEvent.Message:=Format('%s-command failed.',[TextName]);
end;
function TFpDebugThreadCommand.PreExecute(AController: TFpServerDbgController; out DoQueueCommand: boolean): boolean;
begin
DoQueueCommand:=true;
result:=true;
end;
{ TFpDebugThread }
procedure TFpDebugThread.SendLogMessage(const AString: string; const ALogLevel: TFPDLogLevel);
var
ADebugEvent: TFpDebugEvent;
begin
ClearEvent(ADebugEvent);
ADebugEvent.EventType:=etLog;
ADebugEvent.Message:=AString;
ADebugEvent.LogLevel:=ALogLevel;
SendEvent(ADebugEvent);
end;
procedure TFpDebugThread.ClearEvent(var AnEvent: TFpDebugEvent);
begin
AnEvent.AnUID:=null;
AnEvent.SendByConnectionIdentifier:=-1;
AnEvent.InstructionPointerRegValue:=0;
AnEvent.BreakpointServerIdr:=0;
AnEvent.LocationRec.Address:=0;
AnEvent.Validity:=ddsUnknown;
SetLength(AnEvent.StackEntryArray,0);
SetLength(AnEvent.DisassemblerEntryArray,0);
SetLength(AnEvent.WatchEntryArray,0);
AnEvent.Addr1:=0;
AnEvent.Addr2:=0;
AnEvent.Addr3:=0;
end;
procedure TFpDebugThread.FControllerDebugInfoLoaded(Sender: TObject);
begin
//TFpDwarfInfo(FController.CurrentProcess.DbgInfo).MemManager := FMemManager;
end;
procedure TFpDebugThread.FreeConsoleOutputThread;
var
ADebugEvent: TFpDebugEvent;
AThread: TFpWaitForConsoleOutputThread;
begin
if assigned(FConsoleOutputThread) then
begin
AThread := TFpWaitForConsoleOutputThread(FConsoleOutputThread);
FConsoleOutputThread := nil;
AThread.Terminate;
AThread.WaitFor;
AThread.Free;
end;
end;
procedure TFpDebugThread.FControllerHitBreakpointEvent(var continue: boolean;
const Breakpoint: TFpDbgBreakpoint; AnEventType: TFPDEvent; AMoreHitEventsPending: Boolean);
var
ADebugEvent: TFpDebugEvent;
AnId: Integer;
begin
ClearEvent(ADebugEvent);
ADebugEvent.EventType:=etEvent;
ADebugEvent.EventName:='BreakPoint';
ADebugEvent.InstructionPointerRegValue:=FController.CurrentThread.GetInstructionPointerRegisterValue;
if assigned(Breakpoint) then begin
(* There may be several breakpoints at this address.
For now sending the IP address allows the IDE to find the same breakpoint(s) as the fpdserver app.
*)
AnId := FController.GetIdFromInternalBreakPoint(Breakpoint);
ADebugEvent.BreakpointServerIdr := AnId;
end;
SendEvent(ADebugEvent);
continue:=false;
end;
procedure TFpDebugThread.FControllerProcessExitEvent(ExitCode: DWord);
var
ADebugEvent: TFpDebugEvent;
begin
FreeConsoleOutputThread;
ClearEvent(ADebugEvent);
ADebugEvent.EventType:=etEvent;
ADebugEvent.EventName:='ExitProcess';
ADebugEvent.InstructionPointerRegValue:=FController.CurrentThread.GetInstructionPointerRegisterValue;
SendEvent(ADebugEvent);
end;
procedure TFpDebugThread.FControllerCreateProcessEvent(var continue: boolean);
var
ADebugEvent: TFpDebugEvent;
begin
ClearEvent(ADebugEvent);
ADebugEvent.EventType:=etEvent;
ADebugEvent.EventName:='CreateProcess';
ADebugEvent.InstructionPointerRegValue:=FController.CurrentThread.GetInstructionPointerRegisterValue;
SendEvent(ADebugEvent);
if FController.RedirectConsoleOutput then
FConsoleOutputThread := TFpWaitForConsoleOutputThread.Create(self);
continue:=false;
end;
procedure TFpDebugThread.Execute;
var
ACommand: TFpDebugThreadCommand;
ARunLoop: boolean;
AnEvent: TFpDebugEvent;
begin
FController := TFpServerDbgController.Create(FMemManager, FMemManager.MemModel);
FController.RedirectConsoleOutput:=true;
FController.OnCreateProcessEvent:=@FControllerCreateProcessEvent;
FController.OnProcessExitEvent:=@FControllerProcessExitEvent;
FController.OnHitBreakpointEvent:=@FControllerHitBreakpointEvent;
FController.OnDebugInfoLoaded:=@FControllerDebugInfoLoaded;
//TODO: DebugLogger.OnLog ....
//FController.OnLog:=@SendLogMessage;
try
repeat
try
if FCommandQueue.PopItem(ACommand)<>wrSignaled then
ACommand:=nil;
if assigned(ACommand) then
begin
try
ClearEvent(AnEvent);
ACommand.ComposeReceiveEvent(AnEvent);
SendEvent(AnEvent);
if ACommand.Execute(FController, ARunLoop) then
begin
ClearEvent(AnEvent);
ACommand.ComposeSuccessEvent(AnEvent);
SendEvent(AnEvent);
end
else
begin
ClearEvent(AnEvent);
ACommand.ComposeFailureEvent(AnEvent);
SendEvent(AnEvent);
end;
finally
ACommand.Free;
end;
while ARunLoop do
begin
FController.ProcessLoop;
FController.SendEvents(ARunLoop);
end;
end;
except
on E: Exception do
writeln('Exception in debug-thread: '+e.Message); // just continue
end;
until terminated;
finally
FController.Free;
end;
end;
procedure TFpDebugThread.SendEvent(ADebugEvent: TFpDebugEvent);
var
i: integer;
AList: TList;
begin
AList:=FListenerList.LockList;
try
for i := 0 to AList.Count-1 do
begin
IFpDebugListener(AList[i]).SendEvent(ADebugEvent);
end;
finally
FListenerList.UnlockList;
end;
end;
constructor TFpDebugThread.Create;
begin
inherited create(false);
FCommandQueue := TFpDebugThreadCommandQueue.create(100, INFINITE, 100);
FListenerList:=TThreadList.Create;
FMemReader := TFpDbgMemReader.Create(self);
FMemModel := TFpDbgMemModel.Create;
FMemConverter := TFpDbgMemConvertorLittleEndian.Create;
FMemManager := TFpDbgMemManager.Create(FMemReader, FMemConverter, FMemModel);
end;
destructor TFpDebugThread.Destroy;
begin
FreeConsoleOutputThread;
FListenerList.Free;
FCommandQueue.Free;
inherited Destroy;
FMemManager.Free;
FMemConverter.Free;
FMemReader.Free;
FMemModel.Free;
end;
class function TFpDebugThread.Instance: TFpDebugThread;
begin
if not assigned(FFpDebugThread) then
FFpDebugThread:=TFpDebugThread.Create;
result := FFpDebugThread;
end;
procedure TFpDebugThread.QueueCommand(ACommand: TFpDebugThreadCommand);
var
DoQueueCommand: boolean;
Success: boolean;
AnEvent: TFpDebugEvent;
begin
try
Success := ACommand.PreExecute(FController, DoQueueCommand);
except
on E: Exception do
begin
SendLogMessage('Exception while executing command :'+e.Message, dllError);
DoQueueCommand:=false;
Success:=false;
end;
end;
if DoQueueCommand then
begin
FCommandQueue.PushItem(ACommand);
end
else
begin
try
if Success then
begin
ClearEvent(AnEvent);
ACommand.ComposeSuccessEvent(AnEvent);
SendEvent(AnEvent);
end
else
begin
ClearEvent(AnEvent);
ACommand.ComposeFailureEvent(AnEvent);
SendEvent(AnEvent);
end;
finally
ACommand.Free;
end;
end;
end;
procedure TFpDebugThread.SendNotification(AConnectionIdentifier: integer; ANotificationType: TFpDebugNotificationType; AnUID: variant; AMessage, ACommand: string);
var
AnEvent: TFpDebugEvent;
begin
ClearEvent(AnEvent);
AnEvent.SendByConnectionIdentifier:=AConnectionIdentifier;
AnEvent.EventType:=etNotification;
AnEvent.NotificationType:=ANotificationType;
anEvent.EventName:=ACommand;
AnEvent.Message:=AMessage;
AnEvent.AnUID:=AnUID;
SendEvent(AnEvent);
end;
procedure TFpDebugThread.SendNotification(AConnectionIdentifier: integer; ANotificationType: TFpDebugNotificationType; AnUID: variant; AMessage, ACommand: string;
Arg: array of const);
begin
SendNotification(AConnectionIdentifier, ANotificationType, AnUID, format(AMessage, Arg), ACommand);
end;
var
GIdentifierCount: integer = 0;
function TFpDebugThread.AddListener(AFpDebugListener: IFpDebugListener): integer;
begin
inc(GIdentifierCount);
result := GIdentifierCount;
SendNotification(result, ntNewConnection, null, 'New connection from %s', '',[AFpDebugListener.GetOrigin]);
FListenerList.Add(AFpDebugListener);
end;
procedure TFpDebugThread.RemoveListener(AFpDebugListener: IFpDebugListener);
begin
FListenerList.Remove(AFpDebugListener);
end;
initialization
FFpDebugThread := nil;
finalization
FFpDebugThread.Free;
end.