unit GDBMIDebugInstructions;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, math, CmdLineDebugger, GDBMIMiscClasses, LazLoggerBase, LazClasses;

type

  { TGDBMICmdLineDebugger }

  TGDBMICmdLineDebugger = class(TCmdLineDebugger)
  protected
    FErrorHandlingFlags: set of (ehfDeferReadWriteError, ehfGotReadError, ehfGotWriteError);
    procedure DoReadError; override;
    procedure DoWriteError; override;
  end;

  { TGDBInstruction }

  TGDBInstructionFlag = (
    ifRequiresThread,
    ifRequiresStackFrame
  );
  TGDBInstructionFlags = set of TGDBInstructionFlag;

  TGDBInstructionResultFlag = (
    ifrComleted,
    ifrFailed
  );
  TGDBInstructionResultFlags = set of TGDBInstructionResultFlag;

  TGDBInstructionErrorFlag = (
    ifeContentError,  // the imput from gdb was not in the expected format
    ifeWriteError,    // writing to gdb (pipe) failed
    ifeReadError,
    ifeGdbNotRunning,
    ifeTimedOut,
    ifeRecoveredTimedOut, // not an error
    ifeInvalidStackFrame,
    ifeInvalidThreadId,
    ifeQueueContextError  // The thread or stack command went ok, but something else interfered with setting th econtext
  );
  TGDBInstructionErrorFlags = set of TGDBInstructionErrorFlag;

  TGDBInstructionQueue = class;

  { TGDBInstruction }

  TGDBInstruction = class(TRefCountedObject)
  private
    FCommand: String;
    FFlags: TGDBInstructionFlags;
    FStackFrame: Integer;
    FThreadId: Integer;
  protected
    FResultFlags: TGDBInstructionResultFlags;
    FErrorFlags: TGDBInstructionErrorFlags;
    FTimeOut: Integer;
    procedure SendCommandDataToGDB(AQueue: TGDBInstructionQueue); virtual;
    function  ProcessInputFromGdb(const AData: String): Boolean; virtual; abstract; // True if data was handled

    function  GetTimeOutVerifier: TGDBInstruction; virtual;
    procedure Init; virtual;
    procedure InternalCreate(ACommand: String;
                             AThread, AFrame: Integer; // ifRequiresThread, ifRequiresStackFrame will always be included
                             AFlags: TGDBInstructionFlags;
                             ATimeOut: Integer
                            );
  public
    constructor Create(ACommand: String;
                       AFlags: TGDBInstructionFlags = [];
                       ATimeOut: Integer = 0
                      );
    constructor Create(ACommand: String;
                       AThread: Integer;         // ifRequiresThread will always be included
                       AOtherFlags: TGDBInstructionFlags = [];
                       ATimeOut: Integer = 0
                      );
    constructor Create(ACommand: String;
                       AThread, AFrame: Integer; // ifRequiresThread, ifRequiresStackFrame will always be included
                       AOtherFlags: TGDBInstructionFlags = [];
                       ATimeOut: Integer = 0
                      );
    function IsSuccess: Boolean;
    function IsCompleted: boolean; virtual;                                        // No more InputFromGdb required

    procedure MarkAsSuccess;
    procedure HandleWriteError({%H-}ASender: TGDBInstruction); virtual;
    procedure HandleReadError; virtual;
    procedure HandleTimeOut; virtual;
    procedure HandleRecoveredTimeOut; virtual;
    procedure HandleNoGdbRunning; virtual;
    procedure HandleContentError; virtual;
    procedure HandleError(AnError: TGDBInstructionErrorFlag; AMarkAsFailed: Boolean = True); virtual;
    function  DebugText: String;

    property Command: String read FCommand;
    property ThreadId: Integer read FThreadId;
    property StackFrame: Integer read FStackFrame;
    property Flags: TGDBInstructionFlags read FFlags;
    property ResultFlags: TGDBInstructionResultFlags read FResultFlags;
    property ErrorFlags: TGDBInstructionErrorFlags read FErrorFlags;
    property TimeOut: Integer read FTimeOut;
  end;

  { TGDBInstructionVerifyTimeOut }

  TGDBInstructionVerifyTimeOutState = (
    vtSent, vtError,
    vtGotPrompt,
    vtGotPrompt7, vtGotPrompt7gdb, vtGotPrompt7and7, vtGotPrompt7and7gdb,
    vtGotPrompt1, vtGotPrompt1gdb,
    vtGot7, vtGot7gdb, vtGot7and7, vtGot7and7gdb, vtGot1, vtGot1gdb
  );

  TGDBInstructionVerifyTimeOut = class(TGDBInstruction)
  private
    FRunnigInstruction: TGDBInstruction;
    FList: TGDBMINameValueList;
    FPromptAfterErrorCount: Integer;
    FVal7Data: String;
    FState: TGDBInstructionVerifyTimeOutState;
  protected
    procedure SendCommandDataToGDB(AQueue: TGDBInstructionQueue); override;
    function ProcessInputFromGdb(const AData: String): Boolean; override;

    function GetTimeOutVerifier: TGDBInstruction; override;
    function DebugText: String;
  public
    constructor Create(ARunnigInstruction: TGDBInstruction);
    destructor Destroy; override;

    procedure HandleWriteError(ASender: TGDBInstruction); override;
    procedure HandleReadError; override;
    procedure HandleTimeOut; override;
    procedure HandleNoGdbRunning; override;
  end;

  { TGDBInstructionChangeThread }

  TGDBInstructionChangeThread = class(TGDBInstruction)
  private
    FSelThreadId: Integer;
    FQueue: TGDBInstructionQueue;
    FDone: Boolean;
  protected
    procedure SendCommandDataToGDB(AQueue: TGDBInstructionQueue); override;
    function ProcessInputFromGdb(const AData: String): Boolean; override;
    function DebugText: String;
  public
    constructor Create(AQueue: TGDBInstructionQueue; AThreadId: Integer);

    procedure HandleError(AnError: TGDBInstructionErrorFlag; AMarkAsFailed: Boolean = True);
      override;
  end;

  { TGDBInstructionChangeStackFrame }

  TGDBInstructionChangeStackFrame = class(TGDBInstruction)
  private
    FSelStackFrame: Integer;
    FQueue: TGDBInstructionQueue;
    FDone: Boolean;
  protected
    procedure SendCommandDataToGDB(AQueue: TGDBInstructionQueue); override;
    function ProcessInputFromGdb(const AData: String): Boolean; override;
    function DebugText: String;
  public
    constructor Create(AQueue: TGDBInstructionQueue; AFrame: Integer);
    procedure HandleError(AnError: TGDBInstructionErrorFlag; AMarkAsFailed: Boolean = True);
      override;
  end;

  { TGDBInstructionQueue }

  TGDBInstructionQueueFlag = (
    ifqValidThread,
    ifqValidStackFrame
  );
  TGDBInstructionQueueFlags = set of TGDBInstructionQueueFlag;

  TGDBInstructionQueue = class
  private
    FCurrentInstruction: TGDBInstruction;
    FCurrentStackFrame: Integer;
    FCurrentThreadId: Integer;
    FExeCurInstructionStamp: Int64;
    FDebugger: TGDBMICmdLineDebugger;
    FFlags: TGDBInstructionQueueFlags;

    procedure ExecuteCurrentInstruction;
    procedure FinishCurrentInstruction;
    procedure SetCurrentInstruction(AnInstruction: TGDBInstruction);
    function  HasCorrectThreadIdFor(AnInstruction: TGDBInstruction): Boolean;
    function  HasCorrectFrameFor(AnInstruction: TGDBInstruction): Boolean;
  protected
    function SendDataToGDB(ASender: TGDBInstruction; AData: String): Boolean;
    function SendDataToGDB(ASender: TGDBInstruction; AData: String; const AValues: array of const): Boolean;
    procedure HandleGdbDataBeforeInstruction(var {%H-}AData: String; var {%H-}SkipData: Boolean;
                                             const {%H-}TheInstruction: TGDBInstruction); virtual;
    procedure HandleGdbDataAfterInstruction(var {%H-}AData: String; const {%H-}Handled: Boolean;
                                             const {%H-}TheInstruction: TGDBInstruction); virtual;
    function GetSelectThreadInstruction(AThreadId: Integer): TGDBInstruction; virtual;
    function GetSelectFrameInstruction(AFrame: Integer): TGDBInstruction; virtual;

    property Debugger: TGDBMICmdLineDebugger read FDebugger;
  public
    constructor Create(ADebugger: TGDBMICmdLineDebugger);
    procedure InvalidateThredAndFrame(AStackFrameOnly: Boolean = False);
    procedure SetKnownThread(AThread: Integer);
    procedure SetKnownThreadAndFrame(AThread, AFrame: Integer);
    procedure RunInstruction(AnInstruction: TGDBInstruction); // Wait for instruction to be finished, not queuing
    property CurrentThreadId: Integer read FCurrentThreadId;
    property CurrentStackFrame: Integer read FCurrentStackFrame;
    property Flags: TGDBInstructionQueueFlags read FFlags;
  end;

function dbgs(AState: TGDBInstructionVerifyTimeOutState): String; overload;
function dbgs(AFlag: TGDBInstructionQueueFlag): String; overload;
function dbgs(AFlags: TGDBInstructionQueueFlags): String; overload;
function dbgs(AFlag: TGDBInstructionFlag): String; overload;
function dbgs(AFlags: TGDBInstructionFlags): String; overload;

implementation

var
  DBGMI_TIMEOUT_DEBUG, DBG_THREAD_AND_FRAME, DBG_VERBOSE: PLazLoggerLogGroup;

const
  TIMEOUT_AFTER_WRITE_ERROR          = 50;
  TIMEOUT_FOR_QUEUE_INSTR      = 50; // select thread/frame
  TIMEOUT_FOR_SYNC_AFTER_TIMEOUT     = 2500; // extra timeout, while trying to recover from a suspected timeout
  TIMEOUT_FOR_SYNC_AFTER_TIMEOUT_MAX = 3000; // upper limit, when using 2*original_timeout

function dbgs(AState: TGDBInstructionVerifyTimeOutState): String; overload;
begin
  writestr(Result{%H-}, AState);
end;

function dbgs(AFlag: TGDBInstructionQueueFlag): String;
begin
  writestr(Result{%H-}, AFlag);
end;

function dbgs(AFlags: TGDBInstructionQueueFlags): String;
var
  i: TGDBInstructionQueueFlag;
begin
  Result := '';
  for i := low(TGDBInstructionQueueFlags) to high(TGDBInstructionQueueFlags) do
    if i in AFlags then
      if Result = '' then
        Result := Result + dbgs(i)
      else
        Result := Result + ', ' +dbgs(i);
  if Result <> '' then
    Result := '[' + Result + ']';
end;

function dbgs(AFlag: TGDBInstructionFlag): String;
begin
  writestr(Result{%H-}, AFlag);
end;

function dbgs(AFlags: TGDBInstructionFlags): String;
var
  i: TGDBInstructionFlag;
begin
  Result := '';
  for i := low(TGDBInstructionFlags) to high(TGDBInstructionFlags) do
    if i in AFlags then
      if Result = '' then
        Result := Result + dbgs(i)
      else
        Result := Result + ', ' +dbgs(i);
  if Result <> '' then
    Result := '[' + Result + ']';
end;

{ TGDBMICmdLineDebugger }

procedure TGDBMICmdLineDebugger.DoReadError;
begin
  include(FErrorHandlingFlags, ehfGotReadError);
  if not(ehfDeferReadWriteError in FErrorHandlingFlags)
  then inherited DoReadError;
end;

procedure TGDBMICmdLineDebugger.DoWriteError;
begin
  include(FErrorHandlingFlags, ehfGotWriteError);
  if not(ehfDeferReadWriteError in FErrorHandlingFlags)
  then inherited DoWriteError;
end;

{ TGDBInstruction }

procedure TGDBInstruction.SendCommandDataToGDB(AQueue: TGDBInstructionQueue);
begin
  AQueue.SendDataToGDB(Self, FCommand);
end;

function TGDBInstruction.IsCompleted: boolean;
begin
  Result := FResultFlags * [ifrComleted, ifrFailed] <> [];
end;

procedure TGDBInstruction.MarkAsSuccess;
begin
  Include(FResultFlags, ifrComleted);
end;

procedure TGDBInstruction.HandleWriteError(ASender: TGDBInstruction);
begin
  HandleError(ifeWriteError, False);
  if (FTimeOut = 0) or (FTimeOut > TIMEOUT_AFTER_WRITE_ERROR) then
    FTimeOut := TIMEOUT_AFTER_WRITE_ERROR;
end;

procedure TGDBInstruction.HandleReadError;
begin
  HandleError(ifeReadError, True);
end;

procedure TGDBInstruction.HandleTimeOut;
begin
  HandleError(ifeTimedOut, True);
end;

procedure TGDBInstruction.HandleRecoveredTimeOut;
begin
  Include(FErrorFlags, ifeRecoveredTimedOut);
end;

procedure TGDBInstruction.HandleNoGdbRunning;
begin
  HandleError(ifeGdbNotRunning, True);
end;

procedure TGDBInstruction.HandleContentError;
begin
  HandleError(ifeContentError, True);
end;

procedure TGDBInstruction.HandleError(AnError: TGDBInstructionErrorFlag;
  AMarkAsFailed: Boolean = True);
begin
  if AMarkAsFailed then
    Include(FResultFlags, ifrFailed);
  Include(FErrorFlags,  AnError);
end;

function TGDBInstruction.GetTimeOutVerifier: TGDBInstruction;
begin
  if (ifeWriteError in ErrorFlags) then
    Result := nil
  else
    Result := TGDBInstructionVerifyTimeOut.Create(Self);
end;

function TGDBInstruction.DebugText: String;
begin
  Result := ClassName + ': "' + FCommand + '", ' + dbgs(FFlags);
  if ifRequiresThread in FFlags then
    Result := Result + ' Thr=' + IntToStr(FThreadId);
  if ifRequiresStackFrame in FFlags then
    Result := Result + ' Frm=' + IntToStr(FStackFrame);
end;

procedure TGDBInstruction.Init;
begin
  //
end;

procedure TGDBInstruction.InternalCreate(ACommand: String; AThread, AFrame: Integer;
  AFlags: TGDBInstructionFlags; ATimeOut: Integer);
begin
  FCommand := ACommand;
  FThreadId   := AThread;
  FStackFrame := AFrame;
  FFlags := AFlags;
  FTimeOut := ATimeOut;
end;

constructor TGDBInstruction.Create(ACommand: String; AFlags: TGDBInstructionFlags;
  ATimeOut: Integer = 0);
begin
  InternalCreate(ACommand, -1, -1, AFlags, ATimeOut);
  Init;
end;

constructor TGDBInstruction.Create(ACommand: String; AThread: Integer;
  AOtherFlags: TGDBInstructionFlags; ATimeOut: Integer = 0);
begin
  InternalCreate(ACommand, AThread, -1,
                 AOtherFlags + [ifRequiresThread], ATimeOut);
  Init;
end;

constructor TGDBInstruction.Create(ACommand: String; AThread, AFrame: Integer;
  AOtherFlags: TGDBInstructionFlags; ATimeOut: Integer = 0);
begin
  InternalCreate(ACommand, AThread, AFrame,
                 AOtherFlags + [ifRequiresThread, ifRequiresStackFrame], ATimeOut);
  Init;
end;

function TGDBInstruction.IsSuccess: Boolean;
begin
  Result := ResultFlags * [ifrComleted, ifrFailed] = [ifrComleted]
end;

{ TGDBInstructionVerifyTimeOut }

procedure TGDBInstructionVerifyTimeOut.SendCommandDataToGDB(AQueue: TGDBInstructionQueue);
begin
  AQueue.SendDataToGDB(Self, '-data-evaluate-expression 7');
  AQueue.SendDataToGDB(Self, '-data-evaluate-expression 1');
  FState := vtSent;
end;

function TGDBInstructionVerifyTimeOut.ProcessInputFromGdb(const AData: String): Boolean;
type
  TLineDataTipe = (ldOther, ldGdb, ldValue7, ldValue1);

  function CheckData(const ALineData: String): TLineDataTipe;
  begin
    Result := ldOther;
    if ALineData= '(gdb) ' then begin
      Result := ldGdb;
      exit;
    end;
    if (copy(AData, 1, 6) = '^done,') or (AData = '^done') then begin
      if FList = nil then
        FList := TGDBMINameValueList.Create(ALineData)
      else
        FList.Init(ALineData);
      if FList.Values['value'] = '7' then
        Result := ldValue7
      else
      if FList.Values['value'] = '1' then
        Result := ldValue1
    end;
  end;

  procedure SetError(APromptCount: Integer);
  begin
    FState := vtError;
    FPromptAfterErrorCount := APromptCount; // prompt for val7 and val1 needed
    FRunnigInstruction.HandleTimeOut;
    if FPromptAfterErrorCount <= 0 then
      FTimeOut := 50; // wait for timeout
  end;

begin
  Result := True;
  if FState = vtError then begin
    dec(FPromptAfterErrorCount);
    if FPromptAfterErrorCount <= 0 then
      FTimeOut := 50; // wait for timeout
    exit;
  end;

  case CheckData(AData) of
    ldOther: begin
        debugln(DBGMI_TIMEOUT_DEBUG, ['GDBMI TimeOut(', dbgs(FState), '): Got other data']);
        Result := FRunnigInstruction.ProcessInputFromGdb(AData);
      end;
    ldGdb:
      case FState of
        vtSent: begin
            debugln(DBGMI_TIMEOUT_DEBUG, ['GDBMI TimeOut(', dbgs(FState), '): Got prompt in order']);
            FState := vtGotPrompt;
            Result := FRunnigInstruction.ProcessInputFromGdb(AData);
            if not FRunnigInstruction.IsCompleted then begin
              debugln(DBGMI_TIMEOUT_DEBUG, ['GDBMI TimeOut(', dbgs(FState), '): Prompt was not accepted']);
              SetError(2); // prompt for val=7 and val=1 needed
            end;
          end;
        vtGotPrompt7:     FState := vtGotPrompt7gdb;
        vtGotPrompt7and7: FState := vtGotPrompt7and7gdb;
        vtGotPrompt1:     FState := vtGotPrompt1gdb;
        vtGot7:           FState := vtGot7gdb;
        vtGot7and7:       FState := vtGot7and7gdb;
        vtGot1:           FState := vtGot1gdb;
        else begin
            debugln(DBGMI_TIMEOUT_DEBUG, ['GDBMI TimeOut(', dbgs(FState), '): Extra Prompt ']);
            if FState = vtGotPrompt
            then SetError(1)  // prompt val=1 needed
            else SetError(0); // no more prompt needed
          end;
      end;
    ldValue7:
      case FState of
        vtSent, vtGotPrompt: begin
            debugln(DBGMI_TIMEOUT_DEBUG, ['GDBMI TimeOut(', dbgs(FState), '): Got value 7']);
            FVal7Data := AData;
            if FState = vtSent
            then FState := vtGot7
            else FState := vtGotPrompt7;
          end;
        vtGotPrompt7gdb, vtGot7gdb: begin
            debugln(DBGMI_TIMEOUT_DEBUG, ['GDBMI TimeOut(', dbgs(FState), '): Got value 7 twice. Original Result?']);
            Result := FRunnigInstruction.ProcessInputFromGdb(FVal7Data);
            if FState = vtGotPrompt7gdb
            then FState := vtGotPrompt7and7
            else FState := vtGot7and7;
          end;
        else begin
          debugln(DBGMI_TIMEOUT_DEBUG, ['GDBMI TimeOut(', dbgs(FState), '): Extra VAlue 7']);
          if FState in [vtGotPrompt7, vtGot7]
          then SetError(1)  // prompt val=1 needed
          else SetError(0); // no more prompt needed
        end;
      end;
    ldValue1:
      case FState of
        vtSent: begin
          debugln(DBGMI_TIMEOUT_DEBUG, ['GDBMI TimeOut(', dbgs(FState), '): Got other data']);
          Result := FRunnigInstruction.ProcessInputFromGdb(AData);
        end;
        vtGotPrompt7gdb:     FState := vtGotPrompt1;
        vtGotPrompt7and7gdb: FState := vtGotPrompt1;
        vtGot7gdb:           FState := vtGot1;
        vtGot7and7gdb:       FState := vtGot1;
        else begin
          debugln(DBGMI_TIMEOUT_DEBUG, ['GDBMI TimeOut(', dbgs(FState), '): Wrong Value 1']);
          SetError(0);
        end;
      end;
  end;

  if FState = vtGot1gdb then begin
    // timeout, but recovored
    debugln(DBGMI_TIMEOUT_DEBUG, ['GDBMI TimeOut(', dbgs(FState), '): Recovered']);
    FRunnigInstruction.ProcessInputFromGdb('(gdb) '); // simulate prompt
    FRunnigInstruction.HandleRecoveredTimeOut;
  end;
  if FState in [vtGot1gdb, vtGotPrompt1gdb] then begin
    debugln(DBGMI_TIMEOUT_DEBUG, ['GDBMI TimeOut(', dbgs(FState), '): All done: original Instruction completed=',dbgs(FRunnigInstruction.IsCompleted)]);
    Include(FResultFlags, ifrComleted);
    if not FRunnigInstruction.IsCompleted then
      FRunnigInstruction.HandleTimeOut;
  end;

end;

procedure TGDBInstructionVerifyTimeOut.HandleWriteError(ASender: TGDBInstruction);
begin
  inherited HandleWriteError(ASender);
  FRunnigInstruction.HandleWriteError(ASender);
end;

procedure TGDBInstructionVerifyTimeOut.HandleReadError;
begin
  inherited HandleReadError;
  FRunnigInstruction.HandleReadError;
end;

procedure TGDBInstructionVerifyTimeOut.HandleTimeOut;
begin
  inherited HandleTimeOut;
  FRunnigInstruction.HandleTimeOut;
end;

procedure TGDBInstructionVerifyTimeOut.HandleNoGdbRunning;
begin
  inherited HandleNoGdbRunning;
  FRunnigInstruction.HandleNoGdbRunning;
end;

function TGDBInstructionVerifyTimeOut.GetTimeOutVerifier: TGDBInstruction;
begin
  Result := nil;
end;

function TGDBInstructionVerifyTimeOut.DebugText: String;
begin
  Result := ClassName + ': for "' + FRunnigInstruction.DebugText;
end;

constructor TGDBInstructionVerifyTimeOut.Create(ARunnigInstruction: TGDBInstruction);
var
  t: Integer;
begin
  FRunnigInstruction := ARunnigInstruction;
  FRunnigInstruction.AddReference;
  t := FRunnigInstruction.TimeOut;
  t := max(TIMEOUT_FOR_SYNC_AFTER_TIMEOUT, Min(TIMEOUT_FOR_SYNC_AFTER_TIMEOUT_MAX, t * 2));
  inherited Create('', FRunnigInstruction.ThreadId, FRunnigInstruction.StackFrame,
                   FRunnigInstruction.Flags * [ifRequiresThread, ifRequiresStackFrame],
                   t);
end;

destructor TGDBInstructionVerifyTimeOut.Destroy;
begin
  inherited Destroy;
  FreeAndNil(FList);
  if (FRunnigInstruction <> nil) then
    FRunnigInstruction.ReleaseReference;
end;

{ TGDBInstructionChangeThread }

procedure TGDBInstructionChangeThread.SendCommandDataToGDB(AQueue: TGDBInstructionQueue);
begin
  AQueue.SendDataToGDB(Self, '-thread-select %d', [FSelThreadId]);
end;

function TGDBInstructionChangeThread.ProcessInputFromGdb(const AData: String): Boolean;
begin
//  "-thread-select 2"
//  "^done,new-thread-id="2",frame={level="0",addr="0x7707878f",func="ntdll!DbgUiConvertStateChangeStructure",args=[],from="C:\\Windows\\system32\\ntdll.dll"}"
//  "(gdb) "

  Result := False;
  if (copy(AData, 1, 6) = '^done,') or (AData = '^done') then begin
    Result := True;
    if FDone then
      HandleContentError;
    FDone := True;
  end

  else
  if (AData = '(gdb) ') then begin
    Result := True;
    if not FDone then begin
      HandleContentError;
    end
    else begin
      MarkAsSuccess;
      FQueue.FCurrentThreadId := FSelThreadId;
      FQueue.FFlags := FQueue.FFlags + [ifqValidThread];
    end;
  end

  else
  begin
    debugln(DBG_VERBOSE, ['GDBMI TGDBInstructionChangeThread ignoring: ', AData]);
  end;
end;

procedure TGDBInstructionChangeThread.HandleError(AnError: TGDBInstructionErrorFlag;
  AMarkAsFailed: Boolean);
begin
  inherited HandleError(AnError, AMarkAsFailed);
  FQueue.InvalidateThredAndFrame;
end;

function TGDBInstructionChangeThread.DebugText: String;
begin
  Result := ClassName;
end;

constructor TGDBInstructionChangeThread.Create(AQueue: TGDBInstructionQueue;
  AThreadId: Integer);
begin
  inherited Create('', [], TIMEOUT_FOR_QUEUE_INSTR);
  FQueue := AQueue;
  FDone := False;
  FSelThreadId := AThreadId;
end;

{ TGDBInstructionChangeStackFrame }

procedure TGDBInstructionChangeStackFrame.SendCommandDataToGDB(AQueue: TGDBInstructionQueue);
begin
  AQueue.SendDataToGDB(Self, '-stack-select-frame %d', [FSelStackFrame]);
end;

function TGDBInstructionChangeStackFrame.ProcessInputFromGdb(const AData: String): Boolean;
begin
//  "-stack-select-frame 0"
//  "^done"
//  "(gdb) "
//OR ^error => keep selected ?

  Result := False;
  if (copy(AData, 1, 6) = '^done,') or (AData = '^done') then begin
    Result := True;
    if FDone then
      HandleContentError;
    FDone := True;
  end

  else
  if (AData = '(gdb) ') then begin
    Result := True;
    if not FDone then begin
      HandleContentError;
    end
    else begin
      MarkAsSuccess;
      FQueue.FCurrentStackFrame := FSelStackFrame;
      FQueue.FFlags := FQueue.FFlags + [ifqValidStackFrame];
    end;
  end

  else
  begin
    debugln(DBG_VERBOSE, ['GDBMI TGDBInstructionChangeStackFrame ignoring: ', AData]);
  end;
end;

procedure TGDBInstructionChangeStackFrame.HandleError(AnError: TGDBInstructionErrorFlag;
  AMarkAsFailed: Boolean);
begin
  inherited HandleError(AnError, AMarkAsFailed);
  FQueue.InvalidateThredAndFrame(True);
end;

function TGDBInstructionChangeStackFrame.DebugText: String;
begin
  Result := ClassName;
end;

constructor TGDBInstructionChangeStackFrame.Create(AQueue: TGDBInstructionQueue;
  AFrame: Integer);
begin
  inherited Create('', [], TIMEOUT_FOR_QUEUE_INSTR);
  FQueue := AQueue;
  FDone := False;
  FSelStackFrame := AFrame;
end;

{ TGDBInstructionQueue }

procedure TGDBInstructionQueue.ExecuteCurrentInstruction;
var
  ExeInstr, HelpInstr: TGDBInstruction;
  CurStamp: Int64;
begin
  if FCurrentInstruction = nil then
    exit;

  if FExeCurInstructionStamp = high(FExeCurInstructionStamp) then
    FExeCurInstructionStamp := low(FExeCurInstructionStamp)
  else
    inc(FExeCurInstructionStamp);

  ExeInstr := FCurrentInstruction;
  ExeInstr.AddReference;
  try
    while true do begin
      CurStamp := FExeCurInstructionStamp;

      if not HasCorrectThreadIdFor(ExeInstr) then begin
        HelpInstr := GetSelectThreadInstruction(ExeInstr.ThreadId);
        DebugLn(DBG_THREAD_AND_FRAME, ['TGDB_IQ: Changing Thread from: ', FCurrentThreadId, ' - ', dbgs(FFlags),
          ' to ', ExeInstr.ThreadId, ' using [', HelpInstr.DebugText, '] for [', ExeInstr.DebugText, ']']);
        HelpInstr.AddReference;
        try
          FCurrentInstruction := HelpInstr;
          FCurrentInstruction.SendCommandDataToGDB(Self);
          FinishCurrentInstruction;
          if not HelpInstr.IsSuccess then begin
            DebugLn(DBG_THREAD_AND_FRAME, ['TGDB_IQ: Changing Thread FAILED']);
            ExeInstr.HandleError(ifeInvalidThreadId);
            exit;
          end;
        finally
          HelpInstr.ReleaseReference;
        end;
      end;

      if not HasCorrectThreadIdFor(ExeInstr) then begin
        if CurStamp = FExeCurInstructionStamp then begin
          DebugLn(DBG_THREAD_AND_FRAME, ['TGDB_IQ: Thread was interuppted, FAILING']);
          ExeInstr.HandleError(ifeQueueContextError);
          exit;
        end;

        DebugLn(DBG_THREAD_AND_FRAME, ['TGDB_IQ: Thread was interuppted, repeating']);
        continue;
      end;


      if not HasCorrectFrameFor(ExeInstr) then begin
        HelpInstr := GetSelectFrameInstruction(ExeInstr.StackFrame);
        DebugLn(DBG_THREAD_AND_FRAME, ['TGDB_IQ: Changing Stack from: ', FCurrentStackFrame, ' - ', dbgs(FFlags),
          ' to ', ExeInstr.StackFrame, ' using [', HelpInstr.DebugText, '] for [', ExeInstr.DebugText, ']']);
        HelpInstr.AddReference;
        try
          FCurrentInstruction := HelpInstr;
          FCurrentInstruction.SendCommandDataToGDB(Self);
          FinishCurrentInstruction;
          if not HelpInstr.IsSuccess then begin
            DebugLn(DBG_THREAD_AND_FRAME, ['TGDB_IQ: Changing Stackframe FAILED']);
            ExeInstr.HandleError(ifeInvalidStackFrame);
            exit;
          end;
        finally
          HelpInstr.ReleaseReference;
        end;
      end;

      if not (HasCorrectThreadIdFor(ExeInstr) and HasCorrectFrameFor(ExeInstr)) then begin
        if CurStamp = FExeCurInstructionStamp then begin
          DebugLn(DBG_THREAD_AND_FRAME, ['TGDB_IQ: Stack was interuppted, FAILING']);
          ExeInstr.HandleError(ifeQueueContextError);
          exit;
        end;

        DebugLn(DBG_THREAD_AND_FRAME, ['TGDB_IQ: Stack was interuppted, repeating']);
        continue;
      end;

      break;
    end; // while true
  finally
    if (ExeInstr.RefCount > 1) and (not ExeInstr.IsCompleted) then
      FCurrentInstruction := ExeInstr
    else
      FCurrentInstruction := nil;
    ExeInstr.ReleaseReference;
  end;

  if FCurrentInstruction <> nil then
    FCurrentInstruction.SendCommandDataToGDB(Self);
end;

procedure TGDBInstructionQueue.FinishCurrentInstruction;
var
  S: String;
  NewInstr, ExeInstr: TGDBInstruction;
  Skip: Boolean;
  Handled: Boolean;
begin
  if FCurrentInstruction = nil then exit;
  ExeInstr := FCurrentInstruction;
  ExeInstr.AddReference;
  try
    while (FCurrentInstruction <> nil) and
          (not FCurrentInstruction.IsCompleted)
    do begin
      if not FDebugger.DebugProcessRunning then begin
        FCurrentInstruction.HandleNoGdbRunning;
        break;
      end;

      S := FDebugger.ReadLine(FCurrentInstruction.TimeOut);
      // Readline, may go into Application.ProcessMessages.
      // If it does, it has not (yet) read any data.
      // Therefore, if it does, another nested call to readline will work, and data will be returned in the correct order.
      // If a nested readline reads all data, then the outer will have nothing to return.

      if (FCurrentInstruction = nil) or (FCurrentInstruction.IsCompleted) then begin
        if s <> '' then  // Should not happen
          DebugLn(DBG_VERBOSE, ['TGDB_IQ: Got Data, but command was finished. Cmd: ', ExeInstr.DebugText, ' Data: ', S]);
        if not FDebugger.ReadLineWasAbortedByNested then
          DebugLn(DBG_VERBOSE, ['TGDB_IQ: Missing instruction. Not flagged as nested. Cmd: ', ExeInstr.DebugText, ' Data: ', S]);
        break;
      end;

      if FDebugger.ReadLineWasAbortedByNested and (S = '') then
        Continue;

      Skip := False;
      HandleGdbDataBeforeInstruction(S, Skip, FCurrentInstruction);
      // HandleGdbDataBeforeInstruction may execune other Instructions
      if (FCurrentInstruction = nil) or (FCurrentInstruction.IsCompleted) then
        break;

      if (not Skip) and
         ( (not FDebugger.ReadLineTimedOut) or (S <> '') )
      then
        Handled := FCurrentInstruction.ProcessInputFromGdb(S);

      HandleGdbDataAfterInstruction(S, Handled, FCurrentInstruction);

      if (ehfGotReadError in FDebugger.FErrorHandlingFlags) then begin
        FCurrentInstruction.HandleReadError;
        break;
      end;
      if FDebugger.ReadLineTimedOut then begin
        NewInstr := FCurrentInstruction.GetTimeOutVerifier;
        if NewInstr <> nil then begin
          NewInstr.AddReference;
          ExeInstr.ReleaseReference;
          ExeInstr := NewInstr;
          // TODO: Run NewInstr;
          FCurrentInstruction := NewInstr;
          FCurrentInstruction.SendCommandDataToGDB(Self); // ExecuteCurrentInstruction;

        end
        else begin
          FCurrentInstruction.HandleTimeOut;
          break;
        end;
      end;

    end; // while
    FCurrentInstruction := nil;
  finally
    ExeInstr.ReleaseReference;
  end;
end;

procedure TGDBInstructionQueue.SetCurrentInstruction(AnInstruction: TGDBInstruction);
begin
  FinishCurrentInstruction;
  FCurrentInstruction := AnInstruction;
end;

function TGDBInstructionQueue.HasCorrectThreadIdFor(AnInstruction: TGDBInstruction): Boolean;
begin
  Result := not(ifRequiresThread in AnInstruction.Flags);
  if Result then
    exit;
  Result := (ifqValidThread in Flags) and (CurrentThreadId = AnInstruction.ThreadId);
end;

function TGDBInstructionQueue.HasCorrectFrameFor(AnInstruction: TGDBInstruction): Boolean;
begin
  Result := not(ifRequiresStackFrame in AnInstruction.Flags);
  if Result then
    exit;
  Result := (ifqValidStackFrame in Flags) and (CurrentStackFrame = AnInstruction.StackFrame);
end;

function TGDBInstructionQueue.SendDataToGDB(ASender: TGDBInstruction; AData: String): Boolean;
begin
  Result := True;
  FDebugger.FErrorHandlingFlags := FDebugger.FErrorHandlingFlags
    + [ehfDeferReadWriteError] - [ehfGotReadError, ehfGotWriteError];

  FDebugger.SendCmdLn(AData);

  if ehfGotWriteError in FDebugger.FErrorHandlingFlags then begin
    Result := False;
// TODO try reading, but ensure timeout
    if FCurrentInstruction <> nil then
      FCurrentInstruction.HandleWriteError(ASender)
    else
    if ASender <> nil then
      ASender.HandleWriteError(ASender);
  end;
end;

function TGDBInstructionQueue.SendDataToGDB(ASender: TGDBInstruction; AData: String;
  const AValues: array of const): Boolean;
begin
  Result := SendDataToGDB(ASender, Format(AData, AValues));
end;

procedure TGDBInstructionQueue.HandleGdbDataBeforeInstruction(var AData: String;
  var SkipData: Boolean; const TheInstruction: TGDBInstruction);
begin
  //
end;

procedure TGDBInstructionQueue.HandleGdbDataAfterInstruction(var AData: String;
  const Handled: Boolean; const TheInstruction: TGDBInstruction);
begin
  //
end;

function TGDBInstructionQueue.GetSelectThreadInstruction(AThreadId: Integer): TGDBInstruction;
begin
  Result := TGDBInstructionChangeThread.Create(Self, AThreadId);
end;

function TGDBInstructionQueue.GetSelectFrameInstruction(AFrame: Integer): TGDBInstruction;
begin
  Result := TGDBInstructionChangeStackFrame.Create(Self, AFrame);
end;

constructor TGDBInstructionQueue.Create(ADebugger: TGDBMICmdLineDebugger);
begin
  FDebugger := ADebugger;
end;

procedure TGDBInstructionQueue.InvalidateThredAndFrame(AStackFrameOnly: Boolean = False);
begin
  if AStackFrameOnly then begin
    DebugLn(DBG_THREAD_AND_FRAME, ['TGDB_IQ: Invalidating queue''s stack only. Was: ', dbgs(FFlags), ' Thr=', FCurrentThreadId, ' Frm=', FCurrentStackFrame]);
    FFlags := FFlags - [ifqValidStackFrame];
  end
  else begin
    DebugLn(DBG_THREAD_AND_FRAME, ['TGDB_IQ: Invalidating queue''s current thread and stack. Was: ', dbgs(FFlags), ' Thr=', FCurrentThreadId, ' Frm=', FCurrentStackFrame]);
    FFlags := FFlags - [ifqValidThread, ifqValidStackFrame];
  end;
end;

procedure TGDBInstructionQueue.SetKnownThread(AThread: Integer);
begin
  DebugLn(DBG_THREAD_AND_FRAME, ['TGDB_IQ: Setting queue''s current thread and stack. New: Thr=', AThread, ' Was: ', dbgs(FFlags), ' Thr=', FCurrentThreadId, ' Frm=', FCurrentStackFrame]);
  FCurrentThreadId := AThread;
  FFlags := FFlags + [ifqValidThread] - [ifqValidStackFrame];
end;

procedure TGDBInstructionQueue.SetKnownThreadAndFrame(AThread, AFrame: Integer);
begin
  DebugLn(DBG_THREAD_AND_FRAME, ['TGDB_IQ: Setting queue''s current thread and stack. New: Thr=', AThread, ' Frm=', AFrame,' Was: ', dbgs(FFlags), ' Thr=', FCurrentThreadId, ' Frm=', FCurrentStackFrame]);
  FCurrentThreadId := AThread;
  FCurrentStackFrame := AFrame;
  FFlags := FFlags + [ifqValidThread, ifqValidStackFrame];
end;

procedure TGDBInstructionQueue.RunInstruction(AnInstruction: TGDBInstruction);
begin
  SetCurrentInstruction(AnInstruction);
  ExecuteCurrentInstruction;
  FinishCurrentInstruction;
end;

initialization
  DBGMI_TIMEOUT_DEBUG := DebugLogger.RegisterLogGroup('DBGMI_TIMEOUT_DEBUG' {$IFDEF DBGMI_TIMEOUT_DEBUG} , True {$ENDIF} );
  DBG_THREAD_AND_FRAME := DebugLogger.FindOrRegisterLogGroup('DBG_THREAD_AND_FRAME' {$IFDEF DBG_THREAD_AND_FRAME} , True {$ENDIF} );
  DBG_VERBOSE := DebugLogger.FindOrRegisterLogGroup('DBG_VERBOSE' {$IFDEF DBG_VERBOSE} , True {$ENDIF} );

end.

