/**************************************************************************** ** ** Copyright (C) 2018 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of the QtSCriptTools module of the Qt Toolkit. ** ** $QT_BEGIN_LICENSE:LGPL$ ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and The Qt Company. For licensing terms ** and conditions see https://www.qt.io/terms-conditions. For further ** information use the contact form at https://www.qt.io/contact-us. ** ** GNU Lesser General Public License Usage ** Alternatively, this file may be used under the terms of the GNU Lesser ** General Public License version 3 as published by the Free Software ** Foundation and appearing in the file LICENSE.LGPL3 included in the ** packaging of this file. Please review the following information to ** ensure the GNU Lesser General Public License version 3 requirements ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. ** ** GNU General Public License Usage ** Alternatively, this file may be used under the terms of the GNU ** General Public License version 2.0 or (at your option) the GNU General ** Public license version 3 or any later version approved by the KDE Free ** Qt Foundation. The licenses are as published by the Free Software ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 ** included in the packaging of this file. Please review the following ** information to ensure the GNU General Public License requirements will ** be met: https://www.gnu.org/licenses/gpl-2.0.html and ** https://www.gnu.org/licenses/gpl-3.0.html. ** ** $QT_END_LICENSE$ ** ****************************************************************************/ #include "qscriptdebuggeragent_p.h" #include "qscriptdebuggeragent_p_p.h" #include "qscriptdebuggerbackend_p_p.h" #include #include #include QT_BEGIN_NAMESPACE /*! \since 4.5 \class QScriptDebuggerAgent \internal This class implements a state machine that uses the low-level events reported by the QScriptEngineAgent interface to implement debugging- specific functionality such as stepping and breakpoints. It is used internally by the QScriptDebuggerBackend class. */ QScriptDebuggerAgentPrivate::QScriptDebuggerAgentPrivate() : state(NoState), stepDepth(0), stepCount(0), targetScriptId(-1), targetLineNumber(-1), returnCounter(0), nextBreakpointId(1), hitBreakpointId(0), nextContextId(0), statementCounter(0) { } QScriptDebuggerAgentPrivate::~QScriptDebuggerAgentPrivate() { } QScriptDebuggerAgentPrivate *QScriptDebuggerAgentPrivate::get( QScriptDebuggerAgent *q) { if (!q) return 0; return q->d_func(); } /*! Constructs a new agent for the given \a engine. The agent will report debugging-related events (e.g. step completion) to the given \a backend. */ QScriptDebuggerAgent::QScriptDebuggerAgent( QScriptDebuggerBackendPrivate *backend, QScriptEngine *engine) : QScriptEngineAgent(engine), d_ptr(new QScriptDebuggerAgentPrivate()) { Q_D(QScriptDebuggerAgent); d->backend = backend; QScriptContext *ctx = engine->currentContext(); while (ctx) { d->scriptIdStack.append(QList()); d->contextIdStack.append(d->nextContextId); ++d->nextContextId; ctx = ctx->parentContext(); } } /*! Destroys this QScriptDebuggerAgent. */ QScriptDebuggerAgent::~QScriptDebuggerAgent() { Q_D(QScriptDebuggerAgent); if (d->backend) d->backend->agentDestroyed(this); delete d; } /*! Instructs the agent to perform a "step into" operation. This function returns immediately. The agent will report step completion at a later time, i.e. when script statements are evaluated. */ void QScriptDebuggerAgent::enterStepIntoMode(int count) { Q_D(QScriptDebuggerAgent); d->state = QScriptDebuggerAgentPrivate::SteppingIntoState; d->stepCount = count; d->stepResult = QScriptValue(); } /*! Instructs the agent to perform a "step over" operation. This function returns immediately. The agent will report step completion at a later time, i.e. when script statements are evaluated. */ void QScriptDebuggerAgent::enterStepOverMode(int count) { Q_D(QScriptDebuggerAgent); d->state = QScriptDebuggerAgentPrivate::SteppingOverState; if (engine()->isEvaluating()) d->stepDepth = 0; else d->stepDepth = -1; d->stepCount = count; d->stepResult = QScriptValue(); } /*! Instructs the agent to perform a "step out" operation. This function returns immediately. The agent will report step completion at a later time, i.e. when script statements are evaluated. */ void QScriptDebuggerAgent::enterStepOutMode() { Q_D(QScriptDebuggerAgent); d->state = QScriptDebuggerAgentPrivate::SteppingOutState; if (engine()->isEvaluating()) d->stepDepth = 0; else d->stepDepth = -1; } /*! Instructs the agent to continue evaluation. This function returns immediately. */ void QScriptDebuggerAgent::enterContinueMode() { Q_D(QScriptDebuggerAgent); d->state = QScriptDebuggerAgentPrivate::NoState; } /*! Instructs the agent to interrupt evaluation. This function returns immediately. */ void QScriptDebuggerAgent::enterInterruptMode() { Q_D(QScriptDebuggerAgent); d->state = QScriptDebuggerAgentPrivate::InterruptingState; } /*! Instructs the agent to continue evaluation until the location described by \a fileName and \a lineNumber is reached. This function returns immediately. */ void QScriptDebuggerAgent::enterRunToLocationMode(const QString &fileName, int lineNumber) { Q_D(QScriptDebuggerAgent); d->targetFileName = fileName; d->targetLineNumber = lineNumber; d->targetScriptId = resolveScript(fileName); d->state = QScriptDebuggerAgentPrivate::RunningToLocationState; } /*! Instructs the agent to continue evaluation until the location described by \a scriptId and \a lineNumber is reached. This function returns immediately. */ void QScriptDebuggerAgent::enterRunToLocationMode(qint64 scriptId, int lineNumber) { Q_D(QScriptDebuggerAgent); d->targetScriptId = scriptId; d->targetFileName = QString(); d->targetLineNumber = lineNumber; d->state = QScriptDebuggerAgentPrivate::RunningToLocationState; } void QScriptDebuggerAgent::enterReturnByForceMode(int contextIndex, const QScriptValue &value) { Q_D(QScriptDebuggerAgent); d->returnCounter = contextIndex + 1; d->returnValue = QScriptValue(); d->state = QScriptDebuggerAgentPrivate::ReturningByForceState; // throw an exception; we will catch it when the proper frame is popped engine()->currentContext()->throwValue(value); } /*! Sets a breakpoint defined by the given \a data. Returns an integer that uniquely identifies the new breakpoint, or -1 if setting the breakpoint failed. */ int QScriptDebuggerAgent::setBreakpoint(const QScriptBreakpointData &data) { Q_D(QScriptDebuggerAgent); qint64 scriptId = data.scriptId(); if (scriptId != -1) { if (!d->scripts.contains(scriptId)) { // that script has been unloaded, so invalidate the ID scriptId = -1; const_cast(data).setScriptId(-1); } else if (data.fileName().isEmpty()) { QString fileName = d->scripts[scriptId].fileName(); const_cast(data).setFileName(fileName); } } int id = d->nextBreakpointId; ++d->nextBreakpointId; if (scriptId != -1) { d->resolvedBreakpoints[scriptId].append(id); } else { QString fileName = data.fileName(); bool resolved = false; QScriptScriptMap::const_iterator it; for (it = d->scripts.constBegin(); it != d->scripts.constEnd(); ++it) { if (it.value().fileName() == fileName) { d->resolvedBreakpoints[it.key()].append(id); resolved = true; break; } } if (!resolved) d->unresolvedBreakpoints[fileName].append(id); } d->breakpoints.insert(id, data); return id; } /*! Deletes the breakpoint with the given \a id. Returns true if the breakpoint was deleted, false if no such breakpoint exists. */ bool QScriptDebuggerAgent::deleteBreakpoint(int id) { Q_D(QScriptDebuggerAgent); if (!d->breakpoints.contains(id)) return false; d->breakpoints.remove(id); bool found = false; { QHash >::iterator it; it = d->resolvedBreakpoints.begin(); for ( ; !found && (it != d->resolvedBreakpoints.end()); ) { QList &lst = it.value(); Q_ASSERT(!lst.isEmpty()); for (int i = 0; i < lst.size(); ++i) { if (lst.at(i) == id) { lst.removeAt(i); found = true; break; } } if (lst.isEmpty()) it = d->resolvedBreakpoints.erase(it); else ++it; } } if (!found) { QHash >::iterator it; it = d->unresolvedBreakpoints.begin(); for ( ; !found && (it != d->unresolvedBreakpoints.end()); ) { QList &lst = it.value(); Q_ASSERT(!lst.isEmpty()); for (int i = 0; i < lst.size(); ++i) { if (lst.at(i) == id) { lst.removeAt(i); found = true; break; } } if (lst.isEmpty()) it = d->unresolvedBreakpoints.erase(it); else ++it; } } return found; } /*! Deletes all breakpoints. */ void QScriptDebuggerAgent::deleteAllBreakpoints() { Q_D(QScriptDebuggerAgent); d->breakpoints.clear(); d->resolvedBreakpoints.clear(); d->unresolvedBreakpoints.clear(); } /*! Returns the data associated with the breakpoint with the given \a id. */ QScriptBreakpointData QScriptDebuggerAgent::breakpointData(int id) const { Q_D(const QScriptDebuggerAgent); return d->breakpoints.value(id); } /*! Sets the data associated with the breakpoint with the given \a id. */ bool QScriptDebuggerAgent::setBreakpointData(int id, const QScriptBreakpointData &data) { Q_D(QScriptDebuggerAgent); if (!d->breakpoints.contains(id)) return false; d->breakpoints[id] = data; return true; } /*! Returns all breakpoints. */ QScriptBreakpointMap QScriptDebuggerAgent::breakpoints() const { Q_D(const QScriptDebuggerAgent); return d->breakpoints; } /*! Returns all scripts. */ QScriptScriptMap QScriptDebuggerAgent::scripts() const { Q_D(const QScriptDebuggerAgent); return d->scripts; } /*! Returns the data associated with the script with the given \a id. */ QScriptScriptData QScriptDebuggerAgent::scriptData(qint64 id) const { Q_D(const QScriptDebuggerAgent); return d->scripts.value(id); } /*! Checkpoints the current scripts. */ void QScriptDebuggerAgent::scriptsCheckpoint() { Q_D(QScriptDebuggerAgent); d->previousCheckpointScripts = d->checkpointScripts; d->checkpointScripts = d->scripts; } /*! Returns the difference between the current checkpoint and the previous checkpoint. The first item in the pair is a list containing the identifiers of the scripts that were added. The second item in the pair is a list containing the identifiers of the scripts that were removed. */ QPair, QList > QScriptDebuggerAgent::scriptsDelta() const { Q_D(const QScriptDebuggerAgent); QSet prevSet = d->previousCheckpointScripts.keys().toSet(); QSet currSet = d->checkpointScripts.keys().toSet(); QSet addedScriptIds = currSet - prevSet; QSet removedScriptIds = prevSet - currSet; return qMakePair(addedScriptIds.toList(), removedScriptIds.toList()); } /*! Returns the identifier of the script that has the given \a fileName, or -1 if there is no such script. */ qint64 QScriptDebuggerAgent::resolveScript(const QString &fileName) const { Q_D(const QScriptDebuggerAgent); QScriptScriptMap::const_iterator it; for (it = d->scripts.constBegin(); it != d->scripts.constEnd(); ++it) { if (it.value().fileName() == fileName) return it.key(); } return -1; } QList QScriptDebuggerAgent::contextIds() const { Q_D(const QScriptDebuggerAgent); return d->contextIdStack; } QPair, QList > QScriptDebuggerAgent::contextsCheckpoint() { Q_D(QScriptDebuggerAgent); int i = d->checkpointContextIdStack.size() - 1; int j = d->contextIdStack.size() - 1; for ( ; (i >= 0) && (j >= 0); --i, --j) { if (d->checkpointContextIdStack.at(i) != d->contextIdStack.at(j)) break; } QList removed = d->checkpointContextIdStack.mid(0, i+1); QList added = d->contextIdStack.mid(0, j+1); d->checkpointContextIdStack = d->contextIdStack; return qMakePair(removed, added); } void QScriptDebuggerAgent::nullifyBackendPointer() { Q_D(QScriptDebuggerAgent); d->backend = 0; } /*! \reimp */ void QScriptDebuggerAgent::scriptLoad(qint64 id, const QString &program, const QString &fileName, int baseLineNumber) { Q_D(QScriptDebuggerAgent); QScriptScriptData data = QScriptScriptData(program, fileName, baseLineNumber); d->scripts.insert(id, data); if ((d->state == QScriptDebuggerAgentPrivate::RunningToLocationState) && (d->targetScriptId == -1) && ((d->targetFileName == fileName) || d->targetFileName.isEmpty())) { d->targetScriptId = id; } if (!fileName.isEmpty()) { QList lst = d->unresolvedBreakpoints.take(fileName); if (!lst.isEmpty()) d->resolvedBreakpoints.insert(id, lst); } } /*! \reimp */ void QScriptDebuggerAgent::scriptUnload(qint64 id) { Q_D(QScriptDebuggerAgent); QScriptScriptData data = d->scripts.take(id); QString fileName = data.fileName(); if ((d->state == QScriptDebuggerAgentPrivate::RunningToLocationState) && (d->targetScriptId == id)) { d->targetScriptId = -1; d->targetFileName = fileName; } if (!fileName.isEmpty()) { QList lst = d->resolvedBreakpoints.take(id); if (!lst.isEmpty()) d->unresolvedBreakpoints.insert(fileName, lst); } } /*! \reimp */ void QScriptDebuggerAgent::contextPush() { Q_D(QScriptDebuggerAgent); d->scriptIdStack.append(QList()); d->contextIdStack.prepend(d->nextContextId); ++d->nextContextId; } /*! \reimp */ void QScriptDebuggerAgent::contextPop() { Q_D(QScriptDebuggerAgent); d->scriptIdStack.removeLast(); d->contextIdStack.removeFirst(); } /*! \reimp */ void QScriptDebuggerAgent::functionEntry(qint64 scriptId) { Q_D(QScriptDebuggerAgent); QList &ids = d->scriptIdStack.last(); ids.append(scriptId); if ((d->state == QScriptDebuggerAgentPrivate::SteppingOverState) || (d->state == QScriptDebuggerAgentPrivate::SteppingOutState)) { ++d->stepDepth; } } /*! \reimp */ void QScriptDebuggerAgent::functionExit(qint64 scriptId, const QScriptValue &returnValue) { Q_UNUSED(scriptId); Q_D(QScriptDebuggerAgent); QList &ids = d->scriptIdStack.last(); ids.removeLast(); if (d->state == QScriptDebuggerAgentPrivate::SteppingOverState) { --d->stepDepth; } else if (d->state == QScriptDebuggerAgentPrivate::SteppingOutState) { if (--d->stepDepth < 0) { d->stepResult = returnValue; d->state = QScriptDebuggerAgentPrivate::SteppedOutState; } } else if (d->state == QScriptDebuggerAgentPrivate::ReturningByForceState) { if (--d->returnCounter == 0) { d->returnValue = returnValue; d->state = QScriptDebuggerAgentPrivate::ReturnedByForceState; engine()->clearExceptions(); } } } /*! \reimp */ void QScriptDebuggerAgent::positionChange(qint64 scriptId, int lineNumber, int columnNumber) { Q_D(QScriptDebuggerAgent); if (engine()->processEventsInterval() == -1) { // see if it's time to call processEvents() if ((++d->statementCounter % 25000) == 0) { if (!d->processEventsTimer.isNull()) { if (d->processEventsTimer.elapsed() > 30) { QCoreApplication::processEvents(); d->processEventsTimer.restart(); } } else { d->processEventsTimer.start(); } } } // check breakpoints { QList lst = d->resolvedBreakpoints.value(scriptId); for (int i = 0; i < lst.size(); ++i) { int id = lst.at(i); QScriptBreakpointData &data = d->breakpoints[id]; if (!data.isEnabled()) continue; if (data.lineNumber() != lineNumber) continue; if (!data.condition().isEmpty()) { // ### careful, evaluate() can cause an exception // ### disable callbacks in nested evaluate? QScriptDebuggerAgentPrivate::State was = d->state; d->state = QScriptDebuggerAgentPrivate::NoState; QScriptValue ret = engine()->evaluate( data.condition(), QString::fromLatin1("Breakpoint %0 condition checker").arg(id)); if (!ret.isError()) d->state = was; if (!ret.toBoolean()) continue; } if (!data.hit()) continue; d->hitBreakpointId = id; d->state = QScriptDebuggerAgentPrivate::BreakpointState; } } switch (d->state) { case QScriptDebuggerAgentPrivate::NoState: case QScriptDebuggerAgentPrivate::SteppingOutState: case QScriptDebuggerAgentPrivate::ReturningByForceState: // Do nothing break; case QScriptDebuggerAgentPrivate::SteppingIntoState: if (--d->stepCount == 0) { d->state = QScriptDebuggerAgentPrivate::NoState; if (d->backend) d->backend->stepped(scriptId, lineNumber, columnNumber, QScriptValue()); } break; case QScriptDebuggerAgentPrivate::SteppingOverState: if ((d->stepDepth > 0) || (--d->stepCount != 0)) break; // fallthrough case QScriptDebuggerAgentPrivate::SteppedOverState: d->state = QScriptDebuggerAgentPrivate::NoState; if (d->backend) d->backend->stepped(scriptId, lineNumber, columnNumber, d->stepResult); break; case QScriptDebuggerAgentPrivate::SteppedOutState: d->state = QScriptDebuggerAgentPrivate::NoState; if (d->backend) d->backend->stepped(scriptId, lineNumber, columnNumber, d->stepResult); break; case QScriptDebuggerAgentPrivate::RunningToLocationState: if (((lineNumber == d->targetLineNumber) || (d->targetLineNumber == -1)) && (scriptId == d->targetScriptId)) { d->state = QScriptDebuggerAgentPrivate::NoState; if (d->backend) d->backend->locationReached(scriptId, lineNumber, columnNumber); } break; case QScriptDebuggerAgentPrivate::InterruptingState: d->state = QScriptDebuggerAgentPrivate::NoState; if (d->backend) d->backend->interrupted(scriptId, lineNumber, columnNumber); break; case QScriptDebuggerAgentPrivate::BreakpointState: d->state = QScriptDebuggerAgentPrivate::NoState; if (d->backend) d->backend->breakpoint(scriptId, lineNumber, columnNumber, d->hitBreakpointId); if (d->breakpoints.value(d->hitBreakpointId).isSingleShot()) deleteBreakpoint(d->hitBreakpointId); break; case QScriptDebuggerAgentPrivate::ReturnedByForceState: d->state = QScriptDebuggerAgentPrivate::NoState; if (d->backend) d->backend->forcedReturn(scriptId, lineNumber, columnNumber, d->returnValue); break; case QScriptDebuggerAgentPrivate::SteppedIntoState: case QScriptDebuggerAgentPrivate::ReachedLocationState: case QScriptDebuggerAgentPrivate::InterruptedState: // ### deal with the case when code is evaluated while we're already paused // Q_ASSERT(false); break; } } /*! \reimp */ void QScriptDebuggerAgent::exceptionThrow(qint64 scriptId, const QScriptValue &exception, bool hasHandler) { Q_D(QScriptDebuggerAgent); if (d->state == QScriptDebuggerAgentPrivate::ReturningByForceState) { // we threw this exception ourselves, so ignore it for now // (see functionExit()). return; } if (d->backend) d->backend->exception(scriptId, exception, hasHandler); } /*! \reimp */ void QScriptDebuggerAgent::exceptionCatch(qint64 scriptId, const QScriptValue &exception) { Q_UNUSED(scriptId); Q_UNUSED(exception); } /*! \reimp */ bool QScriptDebuggerAgent::supportsExtension(Extension extension) const { return (extension == DebuggerInvocationRequest); } /*! \reimp */ QVariant QScriptDebuggerAgent::extension(Extension extension, const QVariant &argument) { Q_UNUSED(extension); Q_D(QScriptDebuggerAgent); Q_ASSERT(extension == DebuggerInvocationRequest); QVariantList lst = argument.toList(); qint64 scriptId = lst.at(0).toLongLong(); int lineNumber = lst.at(1).toInt(); int columnNumber = lst.at(2).toInt(); d->state = QScriptDebuggerAgentPrivate::NoState; if (d->backend) { d->backend->debuggerInvocationRequest( scriptId, lineNumber, columnNumber); } return QVariant(); } QT_END_NAMESPACE