def cCdbWrapper_fCdbStdInOutThread(oCdbWrapper): # Create a list of commands to set up event handling. The default for any exception not explicitly mentioned is to be # handled as a second chance exception. asExceptionHandlingCommands = ["sxd *"] # request second chance debugger break for certain exceptions that indicate the application has a bug. for sCommand, axExceptions in daxExceptionHandling.items(): for xException in axExceptions: sException = isinstance( xException, str) and xException or ("0x%08X" % xException) asExceptionHandlingCommands.append("%s %s" % (sCommand, sException)) sExceptionHandlingCommands = ";".join(asExceptionHandlingCommands) # Read the initial cdb output related to starting/attaching to the first process. asIntialCdbOutput = oCdbWrapper.fasReadOutput() if not oCdbWrapper.bCdbRunning: return # Turn off prompt information - it's not parsed anyway and clutters output. # Not that +dis and +ea are needed in cErrorReport_foSpecialErrorReport_STATUS_ACCESS_VIOLATION as this causes # an exmpty command to output the oCdbWrapper.fasSendCommandAndReadOutput( ".prompt_allow +dis +ea -reg -src -sym") if not oCdbWrapper.bCdbRunning: return oCdbWrapper.asHTMLCdbStdIOBlocks.pop(-1) # This command is not relevant, remove it from the log. # Exception handlers need to be set up. oCdbWrapper.bExceptionHandlersHaveBeenSet = False # Only fire _fApplicationRunningCallback if the application was started for the first time or resumed after it was # paused to analyze an exception. bInitialApplicationRunningCallbackFired = False bDebuggerNeedsToResumeAttachedProcesses = len( oCdbWrapper.auProcessIdsPendingAttach) > 0 bApplicationWasPausedToAnalyzeAnException = False # An error report will be created when needed; it is returned at the end oErrorReport = None while asIntialCdbOutput or len(oCdbWrapper.auProcessIdsPendingAttach ) + len(oCdbWrapper.auProcessIds) > 0: if asIntialCdbOutput: # First parse the intial output asCdbOutput = asIntialCdbOutput asIntialCdbOutput = None else: # Then attach to a process, or start or resume the application if not bInitialApplicationRunningCallbackFired or bApplicationWasPausedToAnalyzeAnException: # Application was started or resumed after an exception oCdbWrapper.fApplicationRunningCallback and oCdbWrapper.fApplicationRunningCallback( ) bInitialApplicationRunningCallbackFired = True asCdbOutput = oCdbWrapper.fasSendCommandAndReadOutput("g") if not oCdbWrapper.bCdbRunning: return # Save the current number of blocks of StdIO; if this exception is not relevant it can be used to remove all blocks # added while analyzing it. These blocks are not considered to contain useful information and removing them can # reduce the risk of OOM when irrelevant exceptions happens very often. The last block contains a prompt, which # will become the first analysis command's block, so it is not saved. uOriginalHTMLCdbStdIOBlocks = len(oCdbWrapper.asHTMLCdbStdIOBlocks) - 1 # If cdb is attaching to a process, make sure it worked. for sLine in asCdbOutput: oFailedAttachMatch = re.match( r"^Cannot debug pid \d+, Win32 error 0n\d+\s*$", sLine) assert not oFailedAttachMatch, "Failed to attach to process!\r\n%s" % "\r\n".join( asCdbOutput) # Find out what event caused the debugger break asLastEventOutput = oCdbWrapper.fasSendCommandAndReadOutput( ".lastevent") if not oCdbWrapper.bCdbRunning: return # Sample output: # |Last event: 3d8.1348: Create process 3:3d8 # | debugger time: Tue Aug 25 00:06:07.311 2015 (UTC + 2:00) # - or - # |Last event: c74.10e8: Exit process 4:c74, code 0 # | debugger time: Tue Aug 25 00:06:07.311 2015 (UTC + 2:00) bValidLastEventOutput = len(asLastEventOutput) == 2 and re.match( r"^\s*debugger time: .*$", asLastEventOutput[1]) oEventMatch = bValidLastEventOutput and re.match( "".join([ r"^Last event: ([0-9a-f]+)\.[0-9a-f]+: ", r"(?:", r"(Create|Exit) process [0-9a-f]+\:([0-9a-f]+)(?:, code [0-9a-f]+)?", r"|", r"(.*?) \- code ([0-9a-f]+) \(!*\s*(?:first|second) chance\s*!*\)", r")\s*$", ]), asLastEventOutput[0], re.I) assert oEventMatch, "Invalid .lastevent output:\r\n%s" % "\r\n".join( asLastEventOutput) (sProcessIdHex, sCreateExitProcess, sCreateExitProcessIdHex, sExceptionDescription, sExceptionCode) = oEventMatch.groups() uProcessId = long(sProcessIdHex, 16) uExceptionCode = sExceptionCode and int(sExceptionCode, 16) if uExceptionCode in (STATUS_BREAKPOINT, STATUS_WAKE_SYSTEM_DEBUGGER ) and uProcessId not in oCdbWrapper.auProcessIds: # This is assumed to be the initial breakpoint after starting/attaching to the first process or after a new # process was created by the application. This assumption may not be correct, in which case the code needs to # be modifed to check the stack to determine if this really is the initial breakpoint. But that comes at a # performance cost, so until proven otherwise, the code is based on this assumption. sCreateExitProcess = "Create" sCreateExitProcessIdHex = sProcessIdHex if sCreateExitProcess: # Make sure the created/exited process is the current process. assert sProcessIdHex == sCreateExitProcessIdHex, "%s vs %s" % ( sProcessIdHex, sCreateExitProcessIdHex) oCdbWrapper.fHandleCreateExitProcess(sCreateExitProcess, uProcessId) # If there are more processes to attach to, do so: if len(oCdbWrapper.auProcessIdsPendingAttach) > 0: asAttachToProcess = oCdbWrapper.fasSendCommandAndReadOutput( ".attach 0n%d" % oCdbWrapper.auProcessIdsPendingAttach[0]) if not oCdbWrapper.bCdbRunning: return else: # Set up exception handling if this has not been done yet. if not oCdbWrapper.bExceptionHandlersHaveBeenSet: # Note to self: when rewriting the code, make sure not to set up exception handling before the debugger has # attached to all processes. But do so before resuming the threads. Otherwise one or more of the processes can # end up having only one thread that has a suspend count of 2 and no amount of resuming will cause the process # to run. The reason for this is unknown, but if things are done in the correct order, this problem is avoided. oCdbWrapper.bExceptionHandlersHaveBeenSet = True oCdbWrapper.fasSendCommandAndReadOutput( sExceptionHandlingCommands) if not oCdbWrapper.bCdbRunning: return # If the debugger attached to processes, mark that as done and resume threads in all processes. if bDebuggerNeedsToResumeAttachedProcesses: bDebuggerNeedsToResumeAttachedProcesses = False for uProcessId in oCdbWrapper.auProcessIds: oCdbWrapper.fasSendCommandAndReadOutput( "|~[0n%d]s;~*m" % uProcessId) if not oCdbWrapper.bCdbRunning: return # This exception and the commands executed to analyze it are not relevant to the analysis of the bug. As mentioned # above, the commands and their output will be removed from the StdIO array to reduce the risk of OOM. oCdbWrapper.asHTMLCdbStdIOBlocks = ( oCdbWrapper.asHTMLCdbStdIOBlocks[0:uOriginalHTMLCdbStdIOBlocks] + # IO before analysis commands [ "<span class=\"CDBIgnoredException\">%s process %d exception.</span>" % (sCreateExitProcess, uProcessId) ] + # Replacement for analysis commands oCdbWrapper.asHTMLCdbStdIOBlocks[ -1:] # Last block contains prompt and must be conserved. ) else: # Report that the application is paused for analysis... oCdbWrapper.fExceptionDetectedCallback and oCdbWrapper.fExceptionDetectedCallback( uExceptionCode, sExceptionDescription) # And potentially report that the application is resumed later... bApplicationWasPausedToAnalyzeAnException = True # Create an error report, if the exception is fatal. oCdbWrapper.oErrorReport = cErrorReport.foCreate( oCdbWrapper, uExceptionCode, sExceptionDescription) if not oCdbWrapper.bCdbRunning: return if oCdbWrapper.oErrorReport is not None: if dxBugIdConfig["bSaveDump"]: sDumpFileName = fsCreateFileName( oCdbWrapper.oErrorReport.sId) sOverwrite = dxBugIdConfig["bOverwriteDump"] and "/o" or "" oCdbWrapper.fasSendCommandAndReadOutput( ".dump %s /ma \"%s.dmp\"" % (sOverwrite, sDumpFileName)) if not oCdbWrapper.bCdbRunning: return break oCdbWrapper.asHTMLCdbStdIOBlocks = ( oCdbWrapper.asHTMLCdbStdIOBlocks[0:uOriginalHTMLCdbStdIOBlocks] + # IO before analysis commands [ "<span class=\"CDBIgnoredException\">Exception 0x%08X in process %d.</span>" % (uExceptionCode, uProcessId) ] + oCdbWrapper.asHTMLCdbStdIOBlocks[ -1:] # Last block contains prompt and must be conserved. ) # Terminate cdb. oCdbWrapper.bCdbWasTerminatedOnPurpose = True oCdbWrapper.fasSendCommandAndReadOutput("q") assert not oCdbWrapper.bCdbRunning, "Debugger did not terminate when requested"
def cCdbWrapper_fCdbStdInOutThread(oCdbWrapper): # Create a list of commands to set up event handling. The default for any exception not explicitly mentioned is to be # handled as a second chance exception. asExceptionHandlingCommands = ["sxd *"]; # request second chance debugger break for certain exceptions that indicate the application has a bug. for sCommand, axExceptions in daxExceptionHandling.items(): for xException in axExceptions: sException = isinstance(xException, str) and xException or ("0x%08X" % xException); asExceptionHandlingCommands.append("%s %s" % (sCommand, sException)); sExceptionHandlingCommands = ";".join(asExceptionHandlingCommands); # Read the initial cdb output related to starting/attaching to the first process. asIntialCdbOutput = oCdbWrapper.fasReadOutput(); if not oCdbWrapper.bCdbRunning: return; # Turn off prompt information - it's not parsed anyway and clutters output. # Not that +dis and +ea are needed in cErrorReport_foSpecialErrorReport_STATUS_ACCESS_VIOLATION as this causes # an exmpty command to output the oCdbWrapper.fasSendCommandAndReadOutput(".prompt_allow +dis +ea -reg -src -sym"); if not oCdbWrapper.bCdbRunning: return; oCdbWrapper.asHTMLCdbStdIOBlocks.pop(-1); # This command is not relevant, remove it from the log. # Exception handlers need to be set up. oCdbWrapper.bExceptionHandlersHaveBeenSet = False; # Only fire _fApplicationRunningCallback if the application was started for the first time or resumed after it was # paused to analyze an exception. bInitialApplicationRunningCallbackFired = False; bDebuggerNeedsToResumeAttachedProcesses = len(oCdbWrapper.auProcessIdsPendingAttach) > 0; bApplicationWasPausedToAnalyzeAnException = False; # An error report will be created when needed; it is returned at the end oErrorReport = None; while asIntialCdbOutput or len(oCdbWrapper.auProcessIdsPendingAttach) + len(oCdbWrapper.auProcessIds) > 0: if asIntialCdbOutput: # First parse the intial output asCdbOutput = asIntialCdbOutput; asIntialCdbOutput = None; else: # Then attach to a process, or start or resume the application if not bInitialApplicationRunningCallbackFired or bApplicationWasPausedToAnalyzeAnException: # Application was started or resumed after an exception oCdbWrapper.fApplicationRunningCallback and oCdbWrapper.fApplicationRunningCallback(); bInitialApplicationRunningCallbackFired = True; asCdbOutput = oCdbWrapper.fasSendCommandAndReadOutput("g"); if not oCdbWrapper.bCdbRunning: return; # Save the current number of blocks of StdIO; if this exception is not relevant it can be used to remove all blocks # added while analyzing it. These blocks are not considered to contain useful information and removing them can # reduce the risk of OOM when irrelevant exceptions happens very often. The last block contains a prompt, which # will become the first analysis command's block, so it is not saved. uOriginalHTMLCdbStdIOBlocks = len(oCdbWrapper.asHTMLCdbStdIOBlocks) - 1; # If cdb is attaching to a process, make sure it worked. for sLine in asCdbOutput: oFailedAttachMatch = re.match(r"^Cannot debug pid \d+, Win32 error 0n\d+\s*$", sLine); assert not oFailedAttachMatch, "Failed to attach to process!\r\n%s" % "\r\n".join(asCdbOutput); # Find out what event caused the debugger break asLastEventOutput = oCdbWrapper.fasSendCommandAndReadOutput(".lastevent"); if not oCdbWrapper.bCdbRunning: return; # Sample output: # |Last event: 3d8.1348: Create process 3:3d8 # | debugger time: Tue Aug 25 00:06:07.311 2015 (UTC + 2:00) # - or - # |Last event: c74.10e8: Exit process 4:c74, code 0 # | debugger time: Tue Aug 25 00:06:07.311 2015 (UTC + 2:00) bValidLastEventOutput = len(asLastEventOutput) == 2 and re.match(r"^\s*debugger time: .*$", asLastEventOutput[1]); oEventMatch = bValidLastEventOutput and re.match( "".join([ r"^Last event: ([0-9a-f]+)\.[0-9a-f]+: ", r"(?:", r"(Create|Exit) process [0-9a-f]+\:([0-9a-f]+)(?:, code [0-9a-f]+)?", r"|", r"(.*?) \- code ([0-9a-f]+) \(!*\s*(?:first|second) chance\s*!*\)", r")\s*$", ]), asLastEventOutput[0], re.I ); assert oEventMatch, "Invalid .lastevent output:\r\n%s" % "\r\n".join(asLastEventOutput); ( sProcessIdHex, sCreateExitProcess, sCreateExitProcessIdHex, sExceptionDescription, sExceptionCode ) = oEventMatch.groups(); uProcessId = long(sProcessIdHex, 16); uExceptionCode = sExceptionCode and int(sExceptionCode, 16); if uExceptionCode in (STATUS_BREAKPOINT, STATUS_WAKE_SYSTEM_DEBUGGER) and uProcessId not in oCdbWrapper.auProcessIds: # This is assumed to be the initial breakpoint after starting/attaching to the first process or after a new # process was created by the application. This assumption may not be correct, in which case the code needs to # be modifed to check the stack to determine if this really is the initial breakpoint. But that comes at a # performance cost, so until proven otherwise, the code is based on this assumption. sCreateExitProcess = "Create"; sCreateExitProcessIdHex = sProcessIdHex; if sCreateExitProcess: # Make sure the created/exited process is the current process. assert sProcessIdHex == sCreateExitProcessIdHex, "%s vs %s" % (sProcessIdHex, sCreateExitProcessIdHex); oCdbWrapper.fHandleCreateExitProcess(sCreateExitProcess, uProcessId); # If there are more processes to attach to, do so: if len(oCdbWrapper.auProcessIdsPendingAttach) > 0: asAttachToProcess = oCdbWrapper.fasSendCommandAndReadOutput(".attach 0n%d" % oCdbWrapper.auProcessIdsPendingAttach[0]); if not oCdbWrapper.bCdbRunning: return; else: # Set up exception handling if this has not been done yet. if not oCdbWrapper.bExceptionHandlersHaveBeenSet: # Note to self: when rewriting the code, make sure not to set up exception handling before the debugger has # attached to all processes. But do so before resuming the threads. Otherwise one or more of the processes can # end up having only one thread that has a suspend count of 2 and no amount of resuming will cause the process # to run. The reason for this is unknown, but if things are done in the correct order, this problem is avoided. oCdbWrapper.bExceptionHandlersHaveBeenSet = True; oCdbWrapper.fasSendCommandAndReadOutput(sExceptionHandlingCommands); if not oCdbWrapper.bCdbRunning: return; # If the debugger attached to processes, mark that as done and resume threads in all processes. if bDebuggerNeedsToResumeAttachedProcesses: bDebuggerNeedsToResumeAttachedProcesses = False; for uProcessId in oCdbWrapper.auProcessIds: oCdbWrapper.fasSendCommandAndReadOutput("|~[0n%d]s;~*m" % uProcessId); if not oCdbWrapper.bCdbRunning: return; # This exception and the commands executed to analyze it are not relevant to the analysis of the bug. As mentioned # above, the commands and their output will be removed from the StdIO array to reduce the risk of OOM. oCdbWrapper.asHTMLCdbStdIOBlocks = ( oCdbWrapper.asHTMLCdbStdIOBlocks[0:uOriginalHTMLCdbStdIOBlocks] + # IO before analysis commands ["<span class=\"CDBIgnoredException\">%s process %d exception.</span>" % (sCreateExitProcess, uProcessId)] + # Replacement for analysis commands oCdbWrapper.asHTMLCdbStdIOBlocks[-1:] # Last block contains prompt and must be conserved. ); else: # Report that the application is paused for analysis... oCdbWrapper.fExceptionDetectedCallback and oCdbWrapper.fExceptionDetectedCallback(uExceptionCode, sExceptionDescription); # And potentially report that the application is resumed later... bApplicationWasPausedToAnalyzeAnException = True; # Create an error report, if the exception is fatal. oCdbWrapper.oErrorReport = cErrorReport.foCreate(oCdbWrapper, uExceptionCode, sExceptionDescription); if not oCdbWrapper.bCdbRunning: return; if oCdbWrapper.oErrorReport is not None: if dxBugIdConfig["bSaveDump"]: sDumpFileName = fsCreateFileName(oCdbWrapper.oErrorReport.sId); sOverwrite = dxBugIdConfig["bOverwriteDump"] and "/o" or ""; oCdbWrapper.fasSendCommandAndReadOutput(".dump %s /ma \"%s.dmp\"" % (sOverwrite, sDumpFileName)); if not oCdbWrapper.bCdbRunning: return; break; oCdbWrapper.asHTMLCdbStdIOBlocks = ( oCdbWrapper.asHTMLCdbStdIOBlocks[0:uOriginalHTMLCdbStdIOBlocks] + # IO before analysis commands ["<span class=\"CDBIgnoredException\">Exception 0x%08X in process %d.</span>" % (uExceptionCode, uProcessId)] + oCdbWrapper.asHTMLCdbStdIOBlocks[-1:] # Last block contains prompt and must be conserved. ); # Terminate cdb. oCdbWrapper.bCdbWasTerminatedOnPurpose = True; oCdbWrapper.fasSendCommandAndReadOutput("q"); assert not oCdbWrapper.bCdbRunning, "Debugger did not terminate when requested";
def cCdbWrapper_fCdbDebuggerThread(oCdbWrapper): # Read the initial cdb output related to starting/attaching to the first process. asIntialCdbOutput = oCdbWrapper.fasReadOutput(); if not oCdbWrapper.bCdbRunning: return; # Exception handlers need to be set up. oCdbWrapper.bExceptionHandlersHaveBeenSet = False; # Only fire _fApplicationRunningCallback if the application was started for the first time or resumed after it was # paused to analyze an exception. bInitialApplicationRunningCallbackFired = False; bDebuggerNeedsToResumeAttachedProcesses = len(oCdbWrapper.auProcessIdsPendingAttach) > 0; bApplicationWasPausedToAnalyzeAnException = False; # An error report will be created when needed; it is returned at the end oErrorReport = None; while asIntialCdbOutput or len(oCdbWrapper.auProcessIdsPendingAttach) + len(oCdbWrapper.auProcessIds) > 0: if asIntialCdbOutput: # First parse the intial output asCdbOutput = asIntialCdbOutput; asIntialCdbOutput = None; else: # Then attach to a process, or start or resume the application if not bInitialApplicationRunningCallbackFired or bApplicationWasPausedToAnalyzeAnException: # Application was started or resumed after an exception oCdbWrapper.fApplicationRunningCallback and oCdbWrapper.fApplicationRunningCallback(); bInitialApplicationRunningCallbackFired = True; asCdbOutput = oCdbWrapper.fasSendCommandAndReadOutput("g"); if not oCdbWrapper.bCdbRunning: return; # If cdb is attaching to a process, make sure it worked. for sLine in asCdbOutput: oFailedAttachMatch = re.match(r"^Cannot debug pid \d+, Win32 error 0n\d+\s*$", sLine); assert not oFailedAttachMatch, "Failed to attach to process!\r\n%s" % "\r\n".join(asCdbOutput); # Find out what event caused the debugger break asLastEventOutput = oCdbWrapper.fasSendCommandAndReadOutput(".lastevent"); if not oCdbWrapper.bCdbRunning: return; # Sample output: # |Last event: 3d8.1348: Create process 3:3d8 # | debugger time: Tue Aug 25 00:06:07.311 2015 (UTC + 2:00) # - or - # |Last event: c74.10e8: Exit process 4:c74, code 0 # | debugger time: Tue Aug 25 00:06:07.311 2015 (UTC + 2:00) bValidLastEventOutput = len(asLastEventOutput) == 2 and re.match(r"^\s*debugger time: .*$", asLastEventOutput[1]); oEventMatch = bValidLastEventOutput and re.match( "".join([ r"^Last event: ([0-9a-f]+)\.[0-9a-f]+: ", r"(?:", r"(Create|Exit) process [0-9a-f]+\:([0-9a-f]+)(?:, code [0-9a-f]+)?", r"|", r"(.*?) \- code ([0-9a-f]+) \(!*\s*(?:first|second) chance\s*!*\)", r")\s*$", ]), asLastEventOutput[0], re.I ); assert oEventMatch, "Invalid .lastevent output:\r\n%s" % "\r\n".join(asLastEventOutput); ( sProcessIdHex, sCreateExitProcess, sCreateExitProcessIdHex, sExceptionDescription, sExceptionCode ) = oEventMatch.groups(); uProcessId = long(sProcessIdHex, 16); uExceptionCode = sExceptionCode and int(sExceptionCode, 16); if uExceptionCode in (STATUS_BREAKPOINT, STATUS_WAKE_SYSTEM_DEBUGGER) and uProcessId not in oCdbWrapper.auProcessIds: # This is assumed to be the initial breakpoint after starting/attaching to the first process or after a new # process was created by the application. This assumption may not be correct, in which case the code needs to # be modifed to check the stack to determine if this really is the initial breakpoint. But that comes at a # performance cost, so until proven otherwise, the code is based on this assumption. sCreateExitProcess = "Create"; sCreateExitProcessIdHex = sProcessIdHex; if sCreateExitProcess: # Make sure the created/exited process is the current process. assert sProcessIdHex == sCreateExitProcessIdHex, "%s vs %s" % (sProcessIdHex, sCreateExitProcessIdHex); oCdbWrapper.fHandleCreateExitProcess(sCreateExitProcess, uProcessId); # If there are more processes to attach to, do so: if len(oCdbWrapper.auProcessIdsPendingAttach) > 0: asAttachToProcess = oCdbWrapper.fasSendCommandAndReadOutput(".attach 0n%d" % oCdbWrapper.auProcessIdsPendingAttach[0]); if not oCdbWrapper.bCdbRunning: return; else: # Set up exception handling if this has not been done yet. if not oCdbWrapper.bExceptionHandlersHaveBeenSet: # Note to self: when rewriting the code, make sure not to set up exception handling before the debugger has # attached to all processes. But do so before resuming the threads. Otherwise one or more of the processes can # end up having only one thread that has a suspend count of 2 and no amount of resuming will cause the process # to run. The reason for this is unknown, but if things are done in the correct order, this problem is avoided. oCdbWrapper.bExceptionHandlersHaveBeenSet = True; oCdbWrapper.fasSendCommandAndReadOutput(sExceptionHandlingCommands); if not oCdbWrapper.bCdbRunning: return; # If the debugger attached to processes, mark that as done and resume threads in all processes. if bDebuggerNeedsToResumeAttachedProcesses: bDebuggerNeedsToResumeAttachedProcesses = False; for uProcessId in oCdbWrapper.auProcessIds: oCdbWrapper.fasSendCommandAndReadOutput("|~[0n%d]s;~*m" % uProcessId); if not oCdbWrapper.bCdbRunning: return; continue; # Report that the application is paused for analysis... oCdbWrapper.fExceptionDetectedCallback and oCdbWrapper.fExceptionDetectedCallback(uExceptionCode, sExceptionDescription); # And potentially report that the application is resumed later... bApplicationWasPausedToAnalyzeAnException = True; # Optionally perform enhanced symbol reload oCdbWrapper.fEnhancedSymbolReload(); if not oCdbWrapper.bCdbRunning: return; # Create an error report, if the exception is fatal. oCdbWrapper.oErrorReport = cErrorReport.foCreate(oCdbWrapper, uExceptionCode, sExceptionDescription); if not oCdbWrapper.bCdbRunning: return; if oCdbWrapper.oErrorReport is not None: break; # Terminate cdb. oCdbWrapper.bCdbWasTerminatedOnPurpose = True; oCdbWrapper.fasSendCommandAndReadOutput("q"); assert not oCdbWrapper.bCdbRunning, "Debugger did not terminate when requested";