def __init__(self, arguments, earlyConsolePrint, verbose=0): """Constructor. @param arguments GDB start command. """ super(DebuggerIo, self).__init__() self.miParser = MiParser() self.arguments = arguments self.arguments.append("--interpreter=mi") self._miToken = 0 self.onUnknownEvent.connect(self.unknownEvent) self._gdbThread = QProcess() self._gdbThread.setProcessChannelMode(QProcess.MergedChannels) self._gdbThread.error.connect(self.gdbProcessError) self._gdbThread.finished.connect(self.gdbProcessFinished) self._gdbThread.readyReadStandardOutput.connect( self.gdbProcessReadyReadStandardOutput) self._gdbThread.started.connect(self.gdbProcessStarted) self._gdbThread.stateChanged.connect(self.gdbProcessStateChanged) # # Output from the GDB process is read in realtime and written to the # self._lines list. This list is protected by the _linesMutex from the # user-called reader in waitForPrompt(). # self._lines = [] self._linesMutex = QMutex() # # Start. # self._gdbThread.start(self.arguments[0], self.arguments[1:]) self._gdbThread.waitForStarted() self.waitForPrompt("", None, earlyConsolePrint)
def __init__(self, gdbThreadStarted, arguments, verbose=0): """Constructor. @param _gdbThreadStarted Signal completion via semaphore. @param arguments GDB start command. """ super(DebuggerIo, self).__init__() self.miParser = MiParser() self._gdbThreadStarted = gdbThreadStarted self.arguments = arguments self._miToken = 0
def __init__(self, arguments, earlyConsolePrint, verbose = 0): """Constructor. @param arguments GDB start command. """ super(DebuggerIo, self).__init__() self.miParser = MiParser() self.arguments = arguments self.arguments.append("--interpreter=mi") self._miToken = 0 self.onUnknownEvent.connect(self.unknownEvent) self._gdbThread = QProcess() self._gdbThread.setProcessChannelMode(QProcess.MergedChannels) self._gdbThread.error.connect(self.gdbProcessError) self._gdbThread.finished.connect(self.gdbProcessFinished) self._gdbThread.readyReadStandardOutput.connect(self.gdbProcessReadyReadStandardOutput) self._gdbThread.started.connect(self.gdbProcessStarted) self._gdbThread.stateChanged.connect(self.gdbProcessStateChanged) # # Output from the GDB process is read in realtime and written to the # self._lines list. This list is protected by the _linesMutex from the # user-called reader in waitForPrompt(). # self._lines = [] self._linesMutex = QMutex() # # Start. # self._gdbThread.start(self.arguments[0], self.arguments[1:]) self._gdbThread.waitForStarted() self.waitForPrompt("", None, earlyConsolePrint)
def __init__(self, gdbThreadStarted, arguments, verbose = 0): """Constructor. @param _gdbThreadStarted Signal completion via semaphore. @param arguments GDB start command. """ super(DebuggerIo, self).__init__() self.miParser = MiParser() self._gdbThreadStarted = gdbThreadStarted self.arguments = arguments self._miToken = 0
class DebuggerIo(QObject): """A procedural interface to GDB running as a subprocess. A second thread is used to handle I/O with a direct inferior if needed. """ _gdbThread = None _inferiorThread = None _interruptPending = None arguments = None _miToken = None def __init__(self, arguments, earlyConsolePrint, verbose=0): """Constructor. @param arguments GDB start command. """ super(DebuggerIo, self).__init__() self.miParser = MiParser() self.arguments = arguments self.arguments.append("--interpreter=mi") self._miToken = 0 self.onUnknownEvent.connect(self.unknownEvent) self._gdbThread = QProcess() self._gdbThread.setProcessChannelMode(QProcess.MergedChannels) self._gdbThread.error.connect(self.gdbProcessError) self._gdbThread.finished.connect(self.gdbProcessFinished) self._gdbThread.readyReadStandardOutput.connect( self.gdbProcessReadyReadStandardOutput) self._gdbThread.started.connect(self.gdbProcessStarted) self._gdbThread.stateChanged.connect(self.gdbProcessStateChanged) # # Output from the GDB process is read in realtime and written to the # self._lines list. This list is protected by the _linesMutex from the # user-called reader in waitForPrompt(). # self._lines = [] self._linesMutex = QMutex() # # Start. # self._gdbThread.start(self.arguments[0], self.arguments[1:]) self._gdbThread.waitForStarted() self.waitForPrompt("", None, earlyConsolePrint) def interruptWait(self): """Interrupt an in-progress wait for response from GDB.""" self._interruptPending = True def unknownEvent(self, key, args): dbg1("unknown event: {}, {}", key, args) def startIoThread(self): self._inferiorThread = InferiorIo(self) return self._inferiorThread.ttyName() def consoleCommand(self, command, captureConsole=False): """Execute a non-MI command using the GDB/MI interpreter.""" dbg1("consoleCommand: {}", command) if captureConsole: self._captured = [] self.waitForResults("", command, self.consoleCapture) return self._captured else: return self.waitForResults("", command, None) def consoleCapture(self, line): self._captured.append(line) def miCommand(self, command): """Execute a MI command using the GDB/MI interpreter.""" self._miToken += 1 command = "{}{}".format(self._miToken, command) dbg1("miCommand: '{}'", command) return self.waitForResults(str(self._miToken), command, None) def miCommandOne(self, command): """A specialisation of miCommand() where we expect exactly one result record.""" records = self.miCommand(command) return records def miCommandExec(self, command, args): self.miCommandOne(command) def waitForResults(self, token, command, captureConsole, endLine=None, timeoutMs=10000): """Wait for and check results from GDB. @return The result dictionary, or any captureConsole'd output. """ self._gdbThread.write(command + "\n") self._gdbThread.waitForBytesWritten() records = self.waitForPrompt(token, command, captureConsole, endLine, timeoutMs) status, result = records[-1] del records[-1] if status: raise QGdbException("{}: unexpected status {}, {}, {}".format( command, status, result, records)) # # Return the result information and any preceeding records. # if captureConsole: if result: raise QGdbException("{}: unexpected result {}, {}".format( command, result, records)) return records else: if records: raise QGdbException("{}: unexpected records {}, {}".format( command, result, records)) return result def waitForPrompt(self, token, why, captureConsole, endLine=None, timeoutMs=10000): """Read responses from GDB until a prompt, or interrupt. @return lines Each entry in the lines array is either a console string or a parsed dictionary of output. The last entry should be a result. """ prompt = "(gdb) " foundResultOfCommand = not why result = [] lines = [] maxTimeouts = timeoutMs / 100 dbg1("reading for: {}", why) self._interruptPending = False while True: self._linesMutex.lock() tmp = lines lines = self._lines self._lines = tmp self._linesMutex.unlock() while not lines and not self._interruptPending and maxTimeouts: self._gdbThread.waitForReadyRead(100) maxTimeouts -= 1 self._linesMutex.lock() tmp = lines lines = self._lines self._lines = tmp self._linesMutex.unlock() if lines: for i in range(len(lines)): # # TODO: check what IPython does now. # line = lines[i] if endLine and line.startswith(endLine): # # Yay, got to the end! # dbg1( "TODO: check what IPython does: {}: all lines read: {}", why, len(lines)) # # Save any unread lines for next time. # tmp = lines[i + 1:] dbg0("push back {} lines: '{}'", len(tmp), tmp) self._linesMutex.lock() tmp.extend(self._lines) self._lines = tmp self._linesMutex.unlock() result.append(line) return result elif line == prompt: if foundResultOfCommand: # # Yay, got a prompt *after* the result record => got to the end! # dbg1("{}: all lines read: {}", why, len(lines)) # # Save any unread lines for next time, but discard this one # tmp = lines[i + 1:] if tmp: dbg0("push back {} lines: '{}'", len(tmp), tmp) self._linesMutex.lock() tmp.extend(self._lines) self._lines = tmp self._linesMutex.unlock() return result else: dbg1("ignored prompt") elif line[0] == "~": line = self.parseStringRecord(line[1:]) # # GDB console stream record. # if captureConsole: captureConsole(line) else: self.gdbStreamConsole.emit(line) elif line.startswith(token + "^"): # # GDB result-of-command record. # line = line[len(token) + 1:] result.append(self.parseResultRecord(line)) foundResultOfCommand = True else: result.append(line) dbg0("{}: unexpected record string '{}'", why, line) # # We managed to read a line, so reset the timeout. # maxTimeouts = timeoutMs / 100 lines = [] elif self._interruptPending: # # User got fed up. Note, there may be more to read! # raise QGdbInterrupted( "{}: interrupt after {} lines read, {}".format( why, len(result), result)) elif not maxTimeouts: # # Caller got fed up. Note, there may be more to read! # raise QGdbTimeoutError( "{}: timeout after {} lines read, {}".format( why, len(result), result)) def parseStringRecord(self, line): return self.miParser.parse("t=" + line)['t'].strip() def parseOobRecord(self, line): """GDB/MI OOB record.""" dbg2("OOB string {}", line) tuple = line.split(",", 1) if len(tuple) > 1: tuple[1] = self.miParser.parse(tuple[1]) else: tuple.append({}) dbg1("OOB record '{}'", tuple[0]) return tuple def parseResultRecord(self, line): """GDB/MI Result record. @param result "error" for ^error "exit" for ^exit "" for normal cases (^done, ^running, ^connected) @param data "c-string" for ^error "results" for ^done """ dbg2("Result string {}", line) tuple = line.split(",", 1) if tuple[0] in ["done", "running"]: tuple[0] = "" elif tuple[0] == "error": raise QGdbExecuteError(self.miParser.parse(tuple[1])["msg"]) else: raise QGdbException("Unexpected result string '{}'".format(line)) if len(tuple) > 1: tuple[1] = self.miParser.parse(tuple[1]) else: tuple.append({}) dbg1("Result record '{}', '{}'", tuple[0], tuple[1].keys()) return tuple def signalEvent(self, event, args): """Signal any interesting events.""" try: if event == "stopped": self.onStopped.emit(args) elif event == "running": # # This is a string thread id, to allow for the magic value "all". # TODO: A more Pythonic model. # tid = args["thread-id"] self.onRunning.emit(tid) elif event.startswith("thread-group"): tgid = args["id"] if event == "thread-group-added": self.onThreadGroupAdded.emit(tgid) elif event == "thread-group-removed": self.onThreadGroupRemoved.emit(tgid) elif event == "thread-group-started": self.onThreadGroupStarted.emit(tgid, int(args["pid"])) elif event == "thread-group-exited": try: exitCode = int(args["exit-code"]) except KeyError: exitCode = 0 self.onThreadGroupExited.emit(tgid, exitCode) else: self.onUnknownEvent.emit(event, args) elif event.startswith("thread"): tid = int(args["id"]) if event == "thread-created": tgid = args["group-id"] self.onThreadCreated.emit(tid, tgid) elif event == "thread-exited": tgid = args["group-id"] self.onThreadExited.emit(tid, tgid) elif event == "thread-selected": self.onThreadSelected.emit(tid) else: self.onUnknownEvent.emit(event, args) elif event.startswith("library"): lid = args["id"] hostName = args["host-name"] targetName = args["target-name"] tgid = args["thread-group"] if event == "library-loaded": self.onLibraryLoaded.emit(lid, hostName, targetName, int(args["symbols-loaded"]), tgid) elif event == "library-unloaded": self.onLibraryUnloaded.emit(lid, hostName, targetName, tgid) else: self.onUnknownEvent.emit(event, args) elif event.startswith("breakpoint"): if event == "breakpoint-created": self.onBreakpointCreated.emit(args) elif event == "breakpoint-modified": self.onBreakpointModified.emit(args) elif event == "breakpoint-deleted": self.onBreakpointDeleted.emit(args) else: self.onUnknownEvent.emit(event, args) else: self.onUnknownEvent.emit(event, args) except Exception as e: dbg0("unexpected exception: {}: {}", self, e) @pyqtSlot(QProcess.ProcessError) def gdbProcessError(self, error): dbg0("gdbProcessError: {}", error) @pyqtSlot(int, QProcess.ExitStatus) def gdbProcessFinished(self, exitCode, exitStatus): dbg2("gdbProcessFinished: {}, {}", exitCode, exitStatus) @pyqtSlot() def gdbProcessReadyReadStandardOutput(self): dbg2("gdbProcessReadyReadStandardOutput") while self._gdbThread.canReadLine(): line = self._gdbThread.readLine() line = str(line[:-1], "utf-8") # # What kind of line is this, one we have to save, or one we have # to emit right away? # if line[0] == "@": line = self.parseStringRecord(line[1:]) # # Target stream record. Emit now! # self.gdbStreamInferior.emit(line) elif line[0] == "&": line = self.parseStringRecord(line[1:]) # # GDB log stream record. Emit now! # self.gdbStreamLog.emit(line) elif line[0] in ["*", "="]: # # GDB OOB stream record. TODO: does "*" mean inferior state change to stopped? # line = line[1:] tuple = self.parseOobRecord(line) self.signalEvent(tuple[0], tuple[1]) else: # # GDB console stream record (~), # GDB result-of-command record (token^), # or something else (e.g. prompt). # self._linesMutex.lock() self._lines.append(line) self._linesMutex.unlock() @pyqtSlot() def gdbProcessStarted(self): dbg2("gdbProcessStarted") @pyqtSlot(QProcess.ExitStatus) def gdbProcessStateChanged(self, newState): dbg2("gdbProcessStateChanged: {}", newState) """GDB/MI Stream record, GDB console output.""" gdbStreamConsole = pyqtSignal('QString') """GDB/MI Stream record, GDB target output.""" gdbStreamInferior = pyqtSignal('QString') """GDB/MI Stream record, GDB log output.""" gdbStreamLog = pyqtSignal('QString') onUnknownEvent = pyqtSignal('QString', dict) """running,thread-id="all". """ onRunning = pyqtSignal('QString') """stopped,reason="breakpoint-hit",disp="del",bkptno="1",frame={addr="0x4006b0",func="main",args=[{name="argc",value="1"},{name="argv",value="0x7fd48"}],file="dummy.cpp",fullname="dummy.cpp",line="3"},thread-id="1",stopped-threads="all",core="5". """ onStopped = pyqtSignal(dict) """thread-group-added,id="id". """ onThreadGroupAdded = pyqtSignal('QString') """thread-group-removed,id="id". """ onThreadGroupRemoved = pyqtSignal('QString') """thread-group-started,id="id",pid="pid". """ onThreadGroupStarted = pyqtSignal('QString', int) """thread-group-exited,id="id"[,exit-code="code"]. """ onThreadGroupExited = pyqtSignal('QString', int) """thread-created,id="id",group-id="gid". """ onThreadCreated = pyqtSignal(int, 'QString') """thread-exited,id="id",group-id="gid". """ onThreadExited = pyqtSignal(int, 'QString') """thread-selected,id="id". """ onThreadSelected = pyqtSignal(int) """library-loaded,id="id",target-name,host-name,symbols-loaded[,thread-group]. Note: symbols-loaded is not used""" onLibraryLoaded = pyqtSignal('QString', 'QString', 'QString', 'bool', 'QString') """library-unloaded,id="id",target-name,host-name[,thread-group]. """ onLibraryUnloaded = pyqtSignal('QString', 'QString', 'QString', 'QString') """breakpoint-created,bkpt={...}. """ onBreakpointCreated = pyqtSignal(dict) """breakpoint-modified,bkpt={...}. """ onBreakpointModified = pyqtSignal(dict) """breakpoint-deleted,bkpt={...}. """ onBreakpointDeleted = pyqtSignal(dict)
class DebuggerIo(QThread): """A procedural interface to GDB running as a subprocess in a thread. A second thread is used to handle I/O with a direct inferior if needed. """ _gdbThread = None _inferiorThread = None _gdbThreadStarted = None _interruptPending = None arguments = None _miToken = None def __init__(self, gdbThreadStarted, arguments, verbose = 0): """Constructor. @param _gdbThreadStarted Signal completion via semaphore. @param arguments GDB start command. """ super(DebuggerIo, self).__init__() self.miParser = MiParser() self._gdbThreadStarted = gdbThreadStarted self.arguments = arguments self._miToken = 0 def run(self): try: #self._gdbThread = pygdb.Gdb(GDB_CMDLINE, handler = self, verbose = verbose) self._gdbThread = QProcess() self._gdbThread.setProcessChannelMode(QProcess.MergedChannels) self._gdbThread.error.connect(self.gdbProcessError) self._gdbThread.finished.connect(self.gdbProcessFinished) self._gdbThread.readyReadStandardOutput.connect(self.gdbProcessReadyReadStandardOutput) self._gdbThread.started.connect(self.gdbProcessStarted) self._gdbThread.stateChanged.connect(self.gdbProcessStateChanged) # # Start. # self._gdbThread.start(self.arguments[0], self.arguments[1:]) self._gdbThread.waitForStarted() self.waitForPromptConsole("cmd: " + self.arguments[0]) except QGdbException as e: self.dbg0("TODO make signal work: {}", e) traceback.print_exc() self.dbg.emit(0, str(e)) self._gdbThreadStarted.release() def interruptWait(self): """Interrupt an in-progress wait for response from GDB.""" self._interruptPending = True def startIoThread(self): self._inferiorThread = InferiorIo(self) return self._inferiorThread.ttyName() def consoleCommand(self, command): self.dbg1("consoleCommand: {}", command) self._gdbThread.write(command + "\n") self._gdbThread.waitForBytesWritten() return self.waitForPromptConsole(command) def miCommand(self, command): """Execute a command using the GDB/MI interpreter.""" self._miToken += 1 command = "interpreter-exec mi \"{}{}\"".format(self._miToken, command) self.dbg1("miCommand: '{}'", command) self._gdbThread.write(command + "\n") self._gdbThread.waitForBytesWritten() return self.waitForPromptMi(self._miToken, command) def miCommandOne(self, command): """A specialisation of miCommand() where we expect exactly one result record.""" error, records = self.miCommand(command) # # We expect exactly one record. # if error == curses.ascii.CAN: raise QGdbTimeoutError("Timeout after {} results, '{}' ".format(len(records), records)) elif len(records) != 1: raise QGdbInvalidResults("Expected 1 result, not {}, '{}' ".format(len(records), records)) status, results = records[0] self.dbg2("miCommandOne: {}", records[0]) if status == "error": raise QGdbExecuteError(results[0][5:-1]) return results def miCommandExec(self, command, args): self.miCommandOne(command) def waitForPromptConsole(self, why, endLine = None, timeoutMs = 10000): """Read responses from GDB until a prompt, or interrupt. @return (error, lines) Where error is None (normal prompt seen), curses.ascii.ESC (user interrupt) or curses.ascii.CAN (caller timeout) """ prompt = "(gdb) " lines = [] maxTimeouts = timeoutMs / 100 self.dbg1("reading for: {}", why) self._interruptPending = False while True: while not self._gdbThread.canReadLine() and \ self._gdbThread.peek(len(prompt)) != prompt and \ not self._interruptPending and \ maxTimeouts: self._gdbThread.waitForReadyRead(100) maxTimeouts -= 1 if self._gdbThread.canReadLine(): line = self._gdbThread.readLine() line = unicode(line[:-1], "utf-8") lines.append(line) if endLine and line.startswith(endLine): # # Yay, got to the end! # self.dbg2("All lines read: {}", len(lines)) return (None, lines) # # We managed to read a line, so reset the timeout. # maxTimeouts = timeoutMs / 100 elif self._gdbThread.peek(len(prompt)) == prompt: self._gdbThread.read(len(prompt)) # # Yay, got to the end! # self.dbg2("All lines read: {}", len(lines)) return (None, lines) elif self._interruptPending: # # User got fed up. Note, there may be more to read! # self.dbg0("Interrupt after {} lines read, '{}'", len(lines), lines) return (curses.ascii.ESC, lines) elif not maxTimeouts: # # Caller got fed up. Note, there may be more to read! # self.dbg0("Timeout after {} lines read, '{}'", len(lines), lines) return (curses.ascii.CAN, lines) def waitForPromptMi(self, token, why, timeoutMs = 10000): """Read responses from GDB until a prompt, or interrupt. @return (error, lines) Where error is None (normal prompt seen), curses.ascii.ESC (user interrupt) or curses.ascii.CAN (caller timeout) """ prompt = "(gdb) " lines = [] maxTimeouts = timeoutMs / 100 self.dbg1("reading for: {}", why) self._interruptPending = False token = str(token) while True: while not self._gdbThread.canReadLine() and \ self._gdbThread.peek(len(prompt)) != prompt and \ not self._interruptPending and \ maxTimeouts: self._gdbThread.waitForReadyRead(100) maxTimeouts -= 1 if self._gdbThread.canReadLine(): line = self._gdbThread.readLine() line = unicode(line[:-1], "utf-8") if line[0] == "~": line = line[1:] # # Console stream record. Not added to return value! # self.gdbStreamConsole.emit(line) elif line[0] == "@": line = line[1:] # # Target stream record. Not added to return value! # self.gdbStreamTarget.emit(line) elif line[0] == "&": line = line[1:] # # Log stream record. Not added to return value! # self.gdbStreamLog.emit(line) elif line[0] == "*": # # OOB record. # line = line[1:] tuple = self.parseOobRecord(line) self.signalEvent(tuple[0], tuple[1]) lines.append(tuple) elif line.startswith(token + "^"): # # Result record. # line = line[len(token) + 1:] tuple = self.parseResultRecord(line) self.signalEvent(tuple[0], tuple[1]) lines.append(tuple) else: # TODO: other record types. self.dbg0("NYI: unexpected record string {}", line) # # We managed to read a line, so reset the timeout. # maxTimeouts = timeoutMs / 100 elif self._gdbThread.peek(len(prompt)) == prompt: self._gdbThread.read(len(prompt)) # # Yay, got to the end! # self.dbg2("All lines read: {}", len(lines)) return (None, lines) elif self._interruptPending: # # User got fed up. Note, there may be more to read! # self.dbg0("Interrupt after {} lines read", len(lines)) return (curses.ascii.ESC, lines) elif not maxTimeouts: # # Caller got fed up. Note, there may be more to read! # self.dbg0("Timeout after {} lines read", len(lines)) return (curses.ascii.CAN, lines) def parseOobRecord(self, line): """GDB/MI OOB record.""" self.dbg1("OOB string {}", line) tuple = line.split(",", 1) if tuple[0] == "stop": tuple[0] = "" else: self.dbg0("Unexpected OOB string {}", line) if len(tuple) > 1: tuple[1] = self.miParser.parse(tuple[1]) else: tuple.append({}) self.dbg1("OOB record {}", tuple) return tuple def parseResultRecord(self, line): """GDB/MI Result record. @param result "error" for ^error "exit" for ^exit "" for normal cases (^done, ^running, ^connected) @param data "c-string" for ^error "results" for ^done """ self.dbg1("Result string {}", line) tuple = line.split(",", 1) if tuple[0] in ["done", "running" ]: tuple[0] = "" elif tuple[0] != "error": self.dbg0("Unexpected result string {}", line) if len(tuple) > 1: tuple[1] = self.miParser.parse(tuple[1]) else: tuple.append({}) self.dbg1("Result record {}", tuple) return tuple def signalEvent(self, event, args): """Signal whoever is interested of interesting events.""" if event == "stop": self.onStopped.emit(args) elif event.startswith("thread-group"): if event == "thread-group-added": self.onThreadGroupAdded.emit(args) elif event == "thread-group-removed": self.onThreadGroupRemoved.emit(args) elif event == "thread-group-started": self.onThreadGroupStarted.emit(args) elif event == "thread-group-exited": self.onThreadGroupExited.emit(args) else: self.onUnknownEvent.emit(event, args) elif event.startswith("thread"): if event == "thread-created": self.onThreadCreated.emit(args) elif event == "thread-exited": self.onThreadExited.emit(args) elif event == "thread-selected": self.onThreadSelected.emit(args) else: self.onUnknownEvent.emit(event, args) elif event.startswith("library"): if event == "library-loaded": self.onLibraryLoaded.emit(args) elif event == "library-unloaded": self.onLibraryUnloaded.emit(args) else: self.onUnknownEvent.emit(event, args) elif event.startswith("breakpoint"): if event == "breakpoint-created": self.onBreakpointCreated.emit(args) elif event == "breakpoint-modified": self.onBreakpointModified.emit(args) elif event == "breakpoint-deleted": self.onBreakpointDeleted.emit(args) else: self.onUnknownEvent.emit(event, args) else: self.onUnknownEvent.emit(event, args) def dbg0(self, msg, *args): print("ERR-0", msg.format(*args)) def dbg1(self, msg, *args): print("DBG-1", msg.format(*args)) def dbg2(self, msg, *args): print("DBG-2", msg.format(*args)) @pyqtSlot(QProcess.ProcessError) def gdbProcessError(self, error): self.dbg0("gdbProcessError: {}", error) @pyqtSlot(int, QProcess.ExitStatus) def gdbProcessFinished(self, exitCode, exitStatus): self.dbg2("gdbProcessFinished: {}, {}", exitCode, exitStatus) @pyqtSlot() def gdbProcessReadyReadStandardOutput(self): self.dbg2("gdbProcessReadyReadStandardOutput") @pyqtSlot() def gdbProcessStarted(self): self.dbg2("gdbProcessStarted") @pyqtSlot(QProcess.ExitStatus) def gdbProcessStateChanged(self, newState): self.dbg2("gdbProcessStateChanged: {}", newState) """GDB/MI Stream record, GDB console output.""" gdbStreamConsole = pyqtSignal('QString') """GDB/MI Stream record, GDB target output.""" gdbStreamTarget = pyqtSignal('QString') """GDB/MI Stream record, GDB log output.""" gdbStreamLog = pyqtSignal('QString') onUnknownEvent = pyqtSignal('QString', dict) onRunning = pyqtSignal('QString') onStopped = pyqtSignal('QString', 'QString', 'QString', 'QString') """thread-group-added,id="id". """ onThreadGroupAdded = pyqtSignal('QString') """thread-group-removed,id="id". """ onThreadGroupRemoved = pyqtSignal('QString') """thread-group-started,id="id",pid="pid". """ onThreadGroupStarted = pyqtSignal('QString', 'QString') """thread-group-exited,id="id"[,exit-code="code"]. """ onThreadGroupExited = pyqtSignal('QString', 'QString') """thread-created,id="id",group-id="gid". """ onThreadCreated = pyqtSignal('QString', 'QString') """thread-exited,id="id",group-id="gid". """ onThreadExited = pyqtSignal('QString', 'QString') """thread-selected,id="id". """ onThreadSelected = pyqtSignal('QString') """library-loaded,id="id",target-name,host-name,symbols-loaded[,thread-group]. Note: symbols-loaded is not used""" onLibraryLoaded = pyqtSignal('QString', 'QString', 'QString', 'bool', 'QString') """library-unloaded,id="id",target-name,host-name[,thread-group]. """ onLibraryUnloaded = pyqtSignal('QString', 'QString', 'QString', 'QString') """breakpoint-created,bkpt={...}. """ onBreakpointCreated = pyqtSignal('QString') """breakpoint-modified,bkpt={...}. """ onBreakpointModified = pyqtSignal('QString') """breakpoint-deleted,bkpt={...}. """ onBreakpointDeleted = pyqtSignal('QString')
class DebuggerIo(QThread): """A procedural interface to GDB running as a subprocess in a thread. A second thread is used to handle I/O with a direct inferior if needed. """ _gdbThread = None _inferiorThread = None _gdbThreadStarted = None _interruptPending = None arguments = None _miToken = None def __init__(self, gdbThreadStarted, arguments, verbose=0): """Constructor. @param _gdbThreadStarted Signal completion via semaphore. @param arguments GDB start command. """ super(DebuggerIo, self).__init__() self.miParser = MiParser() self._gdbThreadStarted = gdbThreadStarted self.arguments = arguments self._miToken = 0 def run(self): try: #self._gdbThread = pygdb.Gdb(GDB_CMDLINE, handler = self, verbose = verbose) self._gdbThread = QProcess() self._gdbThread.setProcessChannelMode(QProcess.MergedChannels) self._gdbThread.error.connect(self.gdbProcessError) self._gdbThread.finished.connect(self.gdbProcessFinished) self._gdbThread.readyReadStandardOutput.connect( self.gdbProcessReadyReadStandardOutput) self._gdbThread.started.connect(self.gdbProcessStarted) self._gdbThread.stateChanged.connect(self.gdbProcessStateChanged) # # Start. # self._gdbThread.start(self.arguments[0], self.arguments[1:]) self._gdbThread.waitForStarted() self.waitForPromptConsole("cmd: " + self.arguments[0]) except QGdbException as e: self.dbg0("TODO make signal work: {}", e) traceback.print_exc() self.dbg.emit(0, str(e)) self._gdbThreadStarted.release() def interruptWait(self): """Interrupt an in-progress wait for response from GDB.""" self._interruptPending = True def startIoThread(self): self._inferiorThread = InferiorIo(self) return self._inferiorThread.ttyName() def consoleCommand(self, command): self.dbg1("consoleCommand: {}", command) self._gdbThread.write(command + "\n") self._gdbThread.waitForBytesWritten() return self.waitForPromptConsole(command) def miCommand(self, command): """Execute a command using the GDB/MI interpreter.""" self._miToken += 1 command = "interpreter-exec mi \"{}{}\"".format(self._miToken, command) self.dbg1("miCommand: '{}'", command) self._gdbThread.write(command + "\n") self._gdbThread.waitForBytesWritten() return self.waitForPromptMi(self._miToken, command) def miCommandOne(self, command): """A specialisation of miCommand() where we expect exactly one result record.""" error, records = self.miCommand(command) # # We expect exactly one record. # if error == curses.ascii.CAN: raise QGdbTimeoutError("Timeout after {} results, '{}' ".format( len(records), records)) elif len(records) != 1: raise QGdbInvalidResults("Expected 1 result, not {}, '{}' ".format( len(records), records)) status, results = records[0] self.dbg2("miCommandOne: {}", records[0]) if status == "error": raise QGdbExecuteError(results[0][5:-1]) return results def miCommandExec(self, command, args): self.miCommandOne(command) def waitForPromptConsole(self, why, endLine=None, timeoutMs=10000): """Read responses from GDB until a prompt, or interrupt. @return (error, lines) Where error is None (normal prompt seen), curses.ascii.ESC (user interrupt) or curses.ascii.CAN (caller timeout) """ prompt = "(gdb) " lines = [] maxTimeouts = timeoutMs / 100 self.dbg1("reading for: {}", why) self._interruptPending = False while True: while not self._gdbThread.canReadLine() and \ self._gdbThread.peek(len(prompt)) != prompt and \ not self._interruptPending and \ maxTimeouts: self._gdbThread.waitForReadyRead(100) maxTimeouts -= 1 if self._gdbThread.canReadLine(): line = self._gdbThread.readLine() line = unicode(line[:-1], "utf-8") lines.append(line) if endLine and line.startswith(endLine): # # Yay, got to the end! # self.dbg2("All lines read: {}", len(lines)) return (None, lines) # # We managed to read a line, so reset the timeout. # maxTimeouts = timeoutMs / 100 elif self._gdbThread.peek(len(prompt)) == prompt: self._gdbThread.read(len(prompt)) # # Yay, got to the end! # self.dbg2("All lines read: {}", len(lines)) return (None, lines) elif self._interruptPending: # # User got fed up. Note, there may be more to read! # self.dbg0("Interrupt after {} lines read, '{}'", len(lines), lines) return (curses.ascii.ESC, lines) elif not maxTimeouts: # # Caller got fed up. Note, there may be more to read! # self.dbg0("Timeout after {} lines read, '{}'", len(lines), lines) return (curses.ascii.CAN, lines) def waitForPromptMi(self, token, why, timeoutMs=10000): """Read responses from GDB until a prompt, or interrupt. @return (error, lines) Where error is None (normal prompt seen), curses.ascii.ESC (user interrupt) or curses.ascii.CAN (caller timeout) """ prompt = "(gdb) " lines = [] maxTimeouts = timeoutMs / 100 self.dbg1("reading for: {}", why) self._interruptPending = False token = str(token) while True: while not self._gdbThread.canReadLine() and \ self._gdbThread.peek(len(prompt)) != prompt and \ not self._interruptPending and \ maxTimeouts: self._gdbThread.waitForReadyRead(100) maxTimeouts -= 1 if self._gdbThread.canReadLine(): line = self._gdbThread.readLine() line = unicode(line[:-1], "utf-8") if line[0] == "~": line = line[1:] # # Console stream record. Not added to return value! # self.gdbStreamConsole.emit(line) elif line[0] == "@": line = line[1:] # # Target stream record. Not added to return value! # self.gdbStreamTarget.emit(line) elif line[0] == "&": line = line[1:] # # Log stream record. Not added to return value! # self.gdbStreamLog.emit(line) elif line[0] == "*": # # OOB record. # line = line[1:] tuple = self.parseOobRecord(line) self.signalEvent(tuple[0], tuple[1]) lines.append(tuple) elif line.startswith(token + "^"): # # Result record. # line = line[len(token) + 1:] tuple = self.parseResultRecord(line) self.signalEvent(tuple[0], tuple[1]) lines.append(tuple) else: # TODO: other record types. self.dbg0("NYI: unexpected record string {}", line) # # We managed to read a line, so reset the timeout. # maxTimeouts = timeoutMs / 100 elif self._gdbThread.peek(len(prompt)) == prompt: self._gdbThread.read(len(prompt)) # # Yay, got to the end! # self.dbg2("All lines read: {}", len(lines)) return (None, lines) elif self._interruptPending: # # User got fed up. Note, there may be more to read! # self.dbg0("Interrupt after {} lines read", len(lines)) return (curses.ascii.ESC, lines) elif not maxTimeouts: # # Caller got fed up. Note, there may be more to read! # self.dbg0("Timeout after {} lines read", len(lines)) return (curses.ascii.CAN, lines) def parseOobRecord(self, line): """GDB/MI OOB record.""" self.dbg1("OOB string {}", line) tuple = line.split(",", 1) if tuple[0] == "stop": tuple[0] = "" else: self.dbg0("Unexpected OOB string {}", line) if len(tuple) > 1: tuple[1] = self.miParser.parse(tuple[1]) else: tuple.append({}) self.dbg1("OOB record {}", tuple) return tuple def parseResultRecord(self, line): """GDB/MI Result record. @param result "error" for ^error "exit" for ^exit "" for normal cases (^done, ^running, ^connected) @param data "c-string" for ^error "results" for ^done """ self.dbg1("Result string {}", line) tuple = line.split(",", 1) if tuple[0] in ["done", "running"]: tuple[0] = "" elif tuple[0] != "error": self.dbg0("Unexpected result string {}", line) if len(tuple) > 1: tuple[1] = self.miParser.parse(tuple[1]) else: tuple.append({}) self.dbg1("Result record {}", tuple) return tuple def signalEvent(self, event, args): """Signal whoever is interested of interesting events.""" if event == "stop": self.onStopped.emit(args) elif event.startswith("thread-group"): if event == "thread-group-added": self.onThreadGroupAdded.emit(args) elif event == "thread-group-removed": self.onThreadGroupRemoved.emit(args) elif event == "thread-group-started": self.onThreadGroupStarted.emit(args) elif event == "thread-group-exited": self.onThreadGroupExited.emit(args) else: self.onUnknownEvent.emit(event, args) elif event.startswith("thread"): if event == "thread-created": self.onThreadCreated.emit(args) elif event == "thread-exited": self.onThreadExited.emit(args) elif event == "thread-selected": self.onThreadSelected.emit(args) else: self.onUnknownEvent.emit(event, args) elif event.startswith("library"): if event == "library-loaded": self.onLibraryLoaded.emit(args) elif event == "library-unloaded": self.onLibraryUnloaded.emit(args) else: self.onUnknownEvent.emit(event, args) elif event.startswith("breakpoint"): if event == "breakpoint-created": self.onBreakpointCreated.emit(args) elif event == "breakpoint-modified": self.onBreakpointModified.emit(args) elif event == "breakpoint-deleted": self.onBreakpointDeleted.emit(args) else: self.onUnknownEvent.emit(event, args) else: self.onUnknownEvent.emit(event, args) def dbg0(self, msg, *args): print("ERR-0", msg.format(*args)) def dbg1(self, msg, *args): print("DBG-1", msg.format(*args)) def dbg2(self, msg, *args): print("DBG-2", msg.format(*args)) @pyqtSlot(QProcess.ProcessError) def gdbProcessError(self, error): self.dbg0("gdbProcessError: {}", error) @pyqtSlot(int, QProcess.ExitStatus) def gdbProcessFinished(self, exitCode, exitStatus): self.dbg2("gdbProcessFinished: {}, {}", exitCode, exitStatus) @pyqtSlot() def gdbProcessReadyReadStandardOutput(self): self.dbg2("gdbProcessReadyReadStandardOutput") @pyqtSlot() def gdbProcessStarted(self): self.dbg2("gdbProcessStarted") @pyqtSlot(QProcess.ExitStatus) def gdbProcessStateChanged(self, newState): self.dbg2("gdbProcessStateChanged: {}", newState) """GDB/MI Stream record, GDB console output.""" gdbStreamConsole = pyqtSignal('QString') """GDB/MI Stream record, GDB target output.""" gdbStreamTarget = pyqtSignal('QString') """GDB/MI Stream record, GDB log output.""" gdbStreamLog = pyqtSignal('QString') onUnknownEvent = pyqtSignal('QString', dict) onRunning = pyqtSignal('QString') onStopped = pyqtSignal('QString', 'QString', 'QString', 'QString') """thread-group-added,id="id". """ onThreadGroupAdded = pyqtSignal('QString') """thread-group-removed,id="id". """ onThreadGroupRemoved = pyqtSignal('QString') """thread-group-started,id="id",pid="pid". """ onThreadGroupStarted = pyqtSignal('QString', 'QString') """thread-group-exited,id="id"[,exit-code="code"]. """ onThreadGroupExited = pyqtSignal('QString', 'QString') """thread-created,id="id",group-id="gid". """ onThreadCreated = pyqtSignal('QString', 'QString') """thread-exited,id="id",group-id="gid". """ onThreadExited = pyqtSignal('QString', 'QString') """thread-selected,id="id". """ onThreadSelected = pyqtSignal('QString') """library-loaded,id="id",target-name,host-name,symbols-loaded[,thread-group]. Note: symbols-loaded is not used""" onLibraryLoaded = pyqtSignal('QString', 'QString', 'QString', 'bool', 'QString') """library-unloaded,id="id",target-name,host-name[,thread-group]. """ onLibraryUnloaded = pyqtSignal('QString', 'QString', 'QString', 'QString') """breakpoint-created,bkpt={...}. """ onBreakpointCreated = pyqtSignal('QString') """breakpoint-modified,bkpt={...}. """ onBreakpointModified = pyqtSignal('QString') """breakpoint-deleted,bkpt={...}. """ onBreakpointDeleted = pyqtSignal('QString')
class DebuggerIo(QObject): """A procedural interface to GDB running as a subprocess. A second thread is used to handle I/O with a direct inferior if needed. """ _gdbThread = None _inferiorThread = None _interruptPending = None arguments = None _miToken = None def __init__(self, arguments, earlyConsolePrint, verbose = 0): """Constructor. @param arguments GDB start command. """ super(DebuggerIo, self).__init__() self.miParser = MiParser() self.arguments = arguments self.arguments.append("--interpreter=mi") self._miToken = 0 self.onUnknownEvent.connect(self.unknownEvent) self._gdbThread = QProcess() self._gdbThread.setProcessChannelMode(QProcess.MergedChannels) self._gdbThread.error.connect(self.gdbProcessError) self._gdbThread.finished.connect(self.gdbProcessFinished) self._gdbThread.readyReadStandardOutput.connect(self.gdbProcessReadyReadStandardOutput) self._gdbThread.started.connect(self.gdbProcessStarted) self._gdbThread.stateChanged.connect(self.gdbProcessStateChanged) # # Output from the GDB process is read in realtime and written to the # self._lines list. This list is protected by the _linesMutex from the # user-called reader in waitForPrompt(). # self._lines = [] self._linesMutex = QMutex() # # Start. # self._gdbThread.start(self.arguments[0], self.arguments[1:]) self._gdbThread.waitForStarted() self.waitForPrompt("", None, earlyConsolePrint) def interruptWait(self): """Interrupt an in-progress wait for response from GDB.""" self._interruptPending = True def unknownEvent(self, key, args): dbg1("unknown event: {}, {}", key, args) def startIoThread(self): self._inferiorThread = InferiorIo(self) return self._inferiorThread.ttyName() def consoleCommand(self, command, captureConsole = False): """Execute a non-MI command using the GDB/MI interpreter.""" dbg1("consoleCommand: {}", command) if captureConsole: self._captured = [] self.waitForResults("", command, self.consoleCapture) return self._captured else: return self.waitForResults("", command, None) def consoleCapture(self, line): self._captured.append(line) def miCommand(self, command): """Execute a MI command using the GDB/MI interpreter.""" self._miToken += 1 command = "{}{}".format(self._miToken, command) dbg1("miCommand: '{}'", command) return self.waitForResults(str(self._miToken), command, None) def miCommandOne(self, command): """A specialisation of miCommand() where we expect exactly one result record.""" records = self.miCommand(command) return records def miCommandExec(self, command, args): self.miCommandOne(command) def waitForResults(self, token, command, captureConsole, endLine = None, timeoutMs = 10000): """Wait for and check results from GDB. @return The result dictionary, or any captureConsole'd output. """ self._gdbThread.write(command + "\n") self._gdbThread.waitForBytesWritten() records = self.waitForPrompt(token, command, captureConsole, endLine, timeoutMs) status, result = records[-1] del records[-1] if status: raise QGdbException("{}: unexpected status {}, {}, {}".format(command, status, result, records)) # # Return the result information and any preceeding records. # if captureConsole: if result: raise QGdbException("{}: unexpected result {}, {}".format(command, result, records)) return records else: if records: raise QGdbException("{}: unexpected records {}, {}".format(command, result, records)) return result def waitForPrompt(self, token, why, captureConsole, endLine = None, timeoutMs = 10000): """Read responses from GDB until a prompt, or interrupt. @return lines Each entry in the lines array is either a console string or a parsed dictionary of output. The last entry should be a result. """ prompt = "(gdb) " foundResultOfCommand = not why result = [] lines = [] maxTimeouts = timeoutMs / 100 dbg1("reading for: {}", why) self._interruptPending = False while True: self._linesMutex.lock() tmp = lines lines = self._lines self._lines = tmp self._linesMutex.unlock() while not lines and not self._interruptPending and maxTimeouts: self._gdbThread.waitForReadyRead(100) maxTimeouts -= 1 self._linesMutex.lock() tmp = lines lines = self._lines self._lines = tmp self._linesMutex.unlock() if lines: for i in range(len(lines)): # # TODO: check what IPython does now. # line = lines[i] if endLine and line.startswith(endLine): # # Yay, got to the end! # dbg1("TODO: check what IPython does: {}: all lines read: {}", why, len(lines)) # # Save any unread lines for next time. # tmp = lines[i + 1:] dbg0("push back {} lines: '{}'", len(tmp), tmp) self._linesMutex.lock() tmp.extend(self._lines) self._lines = tmp self._linesMutex.unlock() result.append(line) return result elif line == prompt: if foundResultOfCommand: # # Yay, got a prompt *after* the result record => got to the end! # dbg1("{}: all lines read: {}", why, len(lines)) # # Save any unread lines for next time, but discard this one # tmp = lines[i + 1:] if tmp: dbg0("push back {} lines: '{}'", len(tmp), tmp) self._linesMutex.lock() tmp.extend(self._lines) self._lines = tmp self._linesMutex.unlock() return result else: dbg1("ignored prompt") elif line[0] == "~": line = self.parseStringRecord(line[1:]) # # GDB console stream record. # if captureConsole: captureConsole(line) else: self.gdbStreamConsole.emit(line) elif line.startswith(token + "^"): # # GDB result-of-command record. # line = line[len(token) + 1:] result.append(self.parseResultRecord(line)) foundResultOfCommand = True else: result.append(line) dbg0("{}: unexpected record string '{}'", why, line) # # We managed to read a line, so reset the timeout. # maxTimeouts = timeoutMs / 100 lines = [] elif self._interruptPending: # # User got fed up. Note, there may be more to read! # raise QGdbInterrupted("{}: interrupt after {} lines read, {}".format(why, len(result), result)) elif not maxTimeouts: # # Caller got fed up. Note, there may be more to read! # raise QGdbTimeoutError("{}: timeout after {} lines read, {}".format(why, len(result), result)) def parseStringRecord(self, line): return self.miParser.parse("t=" + line)['t'].strip() def parseOobRecord(self, line): """GDB/MI OOB record.""" dbg2("OOB string {}", line) tuple = line.split(",", 1) if len(tuple) > 1: tuple[1] = self.miParser.parse(tuple[1]) else: tuple.append({}) dbg1("OOB record '{}'", tuple[0]) return tuple def parseResultRecord(self, line): """GDB/MI Result record. @param result "error" for ^error "exit" for ^exit "" for normal cases (^done, ^running, ^connected) @param data "c-string" for ^error "results" for ^done """ dbg2("Result string {}", line) tuple = line.split(",", 1) if tuple[0] in ["done", "running"]: tuple[0] = "" elif tuple[0] == "error": raise QGdbExecuteError(self.miParser.parse(tuple[1])["msg"]) else: raise QGdbException("Unexpected result string '{}'".format(line)) if len(tuple) > 1: tuple[1] = self.miParser.parse(tuple[1]) else: tuple.append({}) dbg1("Result record '{}', '{}'", tuple[0], tuple[1].keys()) return tuple def signalEvent(self, event, args): """Signal any interesting events.""" try: if event == "stopped": self.onStopped.emit(args) elif event == "running": # # This is a string thread id, to allow for the magic value "all". # TODO: A more Pythonic model. # tid = args["thread-id"] self.onRunning.emit(tid) elif event.startswith("thread-group"): tgid = args["id"] if event == "thread-group-added": self.onThreadGroupAdded.emit(tgid) elif event == "thread-group-removed": self.onThreadGroupRemoved.emit(tgid) elif event == "thread-group-started": self.onThreadGroupStarted.emit(tgid, int(args["pid"])) elif event == "thread-group-exited": try: exitCode = int(args["exit-code"]) except KeyError: exitCode = 0 self.onThreadGroupExited.emit(tgid, exitCode) else: self.onUnknownEvent.emit(event, args) elif event.startswith("thread"): tid = int(args["id"]) if event == "thread-created": tgid = args["group-id"] self.onThreadCreated.emit(tid, tgid) elif event == "thread-exited": tgid = args["group-id"] self.onThreadExited.emit(tid, tgid) elif event == "thread-selected": self.onThreadSelected.emit(tid) else: self.onUnknownEvent.emit(event, args) elif event.startswith("library"): lid = args["id"] hostName = args["host-name"] targetName = args["target-name"] tgid = args["thread-group"] if event == "library-loaded": self.onLibraryLoaded.emit(lid, hostName, targetName, int(args["symbols-loaded"]), tgid) elif event == "library-unloaded": self.onLibraryUnloaded.emit(lid, hostName, targetName, tgid) else: self.onUnknownEvent.emit(event, args) elif event.startswith("breakpoint"): if event == "breakpoint-created": self.onBreakpointCreated.emit(args) elif event == "breakpoint-modified": self.onBreakpointModified.emit(args) elif event == "breakpoint-deleted": self.onBreakpointDeleted.emit(args) else: self.onUnknownEvent.emit(event, args) else: self.onUnknownEvent.emit(event, args) except Exception as e: dbg0("unexpected exception: {}: {}", self, e) @pyqtSlot(QProcess.ProcessError) def gdbProcessError(self, error): dbg0("gdbProcessError: {}", error) @pyqtSlot(int, QProcess.ExitStatus) def gdbProcessFinished(self, exitCode, exitStatus): dbg2("gdbProcessFinished: {}, {}", exitCode, exitStatus) @pyqtSlot() def gdbProcessReadyReadStandardOutput(self): dbg2("gdbProcessReadyReadStandardOutput") while self._gdbThread.canReadLine(): line = self._gdbThread.readLine() line = str(line[:-1], "utf-8") # # What kind of line is this, one we have to save, or one we have # to emit right away? # if line[0] == "@": line = self.parseStringRecord(line[1:]) # # Target stream record. Emit now! # self.gdbStreamInferior.emit(line) elif line[0] == "&": line = self.parseStringRecord(line[1:]) # # GDB log stream record. Emit now! # self.gdbStreamLog.emit(line) elif line[0] in ["*", "="]: # # GDB OOB stream record. TODO: does "*" mean inferior state change to stopped? # line = line[1:] tuple = self.parseOobRecord(line) self.signalEvent(tuple[0], tuple[1]) else: # # GDB console stream record (~), # GDB result-of-command record (token^), # or something else (e.g. prompt). # self._linesMutex.lock() self._lines.append(line) self._linesMutex.unlock() @pyqtSlot() def gdbProcessStarted(self): dbg2("gdbProcessStarted") @pyqtSlot(QProcess.ExitStatus) def gdbProcessStateChanged(self, newState): dbg2("gdbProcessStateChanged: {}", newState) """GDB/MI Stream record, GDB console output.""" gdbStreamConsole = pyqtSignal('QString') """GDB/MI Stream record, GDB target output.""" gdbStreamInferior = pyqtSignal('QString') """GDB/MI Stream record, GDB log output.""" gdbStreamLog = pyqtSignal('QString') onUnknownEvent = pyqtSignal('QString', dict) """running,thread-id="all". """ onRunning = pyqtSignal('QString') """stopped,reason="breakpoint-hit",disp="del",bkptno="1",frame={addr="0x4006b0",func="main",args=[{name="argc",value="1"},{name="argv",value="0x7fd48"}],file="dummy.cpp",fullname="dummy.cpp",line="3"},thread-id="1",stopped-threads="all",core="5". """ onStopped = pyqtSignal(dict) """thread-group-added,id="id". """ onThreadGroupAdded = pyqtSignal('QString') """thread-group-removed,id="id". """ onThreadGroupRemoved = pyqtSignal('QString') """thread-group-started,id="id",pid="pid". """ onThreadGroupStarted = pyqtSignal('QString', int) """thread-group-exited,id="id"[,exit-code="code"]. """ onThreadGroupExited = pyqtSignal('QString', int) """thread-created,id="id",group-id="gid". """ onThreadCreated = pyqtSignal(int, 'QString') """thread-exited,id="id",group-id="gid". """ onThreadExited = pyqtSignal(int, 'QString') """thread-selected,id="id". """ onThreadSelected = pyqtSignal(int) """library-loaded,id="id",target-name,host-name,symbols-loaded[,thread-group]. Note: symbols-loaded is not used""" onLibraryLoaded = pyqtSignal('QString', 'QString', 'QString', 'bool', 'QString') """library-unloaded,id="id",target-name,host-name[,thread-group]. """ onLibraryUnloaded = pyqtSignal('QString', 'QString', 'QString', 'QString') """breakpoint-created,bkpt={...}. """ onBreakpointCreated = pyqtSignal(dict) """breakpoint-modified,bkpt={...}. """ onBreakpointModified = pyqtSignal(dict) """breakpoint-deleted,bkpt={...}. """ onBreakpointDeleted = pyqtSignal(dict)