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; 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; { 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.