Example #1
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)
Example #2
0
    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
Example #3
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)
Example #4
0
    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
Example #5
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)
Example #6
0
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')
Example #7
0
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')
Example #8
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)