blob: 6037c3ee7d8d3d54b21098fb5a779dc2afb2faef [file] [log] [blame]
/****************************************************************************
**
** Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies).
** All rights reserved.
** Contact: Nokia Corporation (qt-info@nokia.com)
**
** This file is part of the QtSCriptTools module of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL$
** GNU Lesser General Public License Usage
** This file may be used under the terms of the GNU Lesser General Public
** License version 2.1 as published by the Free Software Foundation and
** appearing in the file LICENSE.LGPL included in the packaging of this
** file. Please review the following information to ensure the GNU Lesser
** General Public License version 2.1 requirements will be met:
** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
**
** In addition, as a special exception, Nokia gives you certain additional
** rights. These rights are described in the Nokia Qt LGPL Exception
** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU General
** Public License version 3.0 as published by the Free Software Foundation
** and appearing in the file LICENSE.GPL included in the packaging of this
** file. Please review the following information to ensure the GNU General
** Public License version 3.0 requirements will be met:
** http://www.gnu.org/copyleft/gpl.html.
**
** Other Usage
** Alternatively, this file may be used in accordance with the terms and
** conditions contained in a signed written agreement between you and Nokia.
**
**
**
**
**
** $QT_END_LICENSE$
**
****************************************************************************/
#include "qscriptdebuggeragent_p.h"
#include "qscriptdebuggeragent_p_p.h"
#include "qscriptdebuggerbackend_p_p.h"
#include <QtCore/qcoreapplication.h>
#include <QtCore/qset.h>
#include <QtScript/qscriptengine.h>
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<qint64>());
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<QScriptBreakpointData&>(data).setScriptId(-1);
} else if (data.fileName().isEmpty()) {
QString fileName = d->scripts[scriptId].fileName();
const_cast<QScriptBreakpointData&>(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<qint64, QList<int> >::iterator it;
it = d->resolvedBreakpoints.begin();
for ( ; !found && (it != d->resolvedBreakpoints.end()); ) {
QList<int> &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<QString, QList<int> >::iterator it;
it = d->unresolvedBreakpoints.begin();
for ( ; !found && (it != d->unresolvedBreakpoints.end()); ) {
QList<int> &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<qint64>, QList<qint64> > QScriptDebuggerAgent::scriptsDelta() const
{
Q_D(const QScriptDebuggerAgent);
QSet<qint64> prevSet = d->previousCheckpointScripts.keys().toSet();
QSet<qint64> currSet = d->checkpointScripts.keys().toSet();
QSet<qint64> addedScriptIds = currSet - prevSet;
QSet<qint64> 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<qint64> QScriptDebuggerAgent::contextIds() const
{
Q_D(const QScriptDebuggerAgent);
return d->contextIdStack;
}
QPair<QList<qint64>, QList<qint64> > 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<qint64> removed = d->checkpointContextIdStack.mid(0, i+1);
QList<qint64> 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<int> 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<int> lst = d->resolvedBreakpoints.take(id);
if (!lst.isEmpty())
d->unresolvedBreakpoints.insert(fileName, lst);
}
}
/*!
\reimp
*/
void QScriptDebuggerAgent::contextPush()
{
Q_D(QScriptDebuggerAgent);
d->scriptIdStack.append(QList<qint64>());
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<qint64> &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<qint64> &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<int> 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