blob: c2626f37d09bcd1c7346ed216fc8538f37549fc7 [file] [log] [blame]
/****************************************************************************
**
** Copyright (C) 2017 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the examples of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:BSD$
** 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.
**
** BSD License Usage
** Alternatively, you may use this file under the terms of the BSD license
** as follows:
**
** "Redistribution and use in source and binary forms, with or without
** modification, are permitted provided that the following conditions are
** met:
** * Redistributions of source code must retain the above copyright
** notice, this list of conditions and the following disclaimer.
** * Redistributions in binary form must reproduce the above copyright
** notice, this list of conditions and the following disclaimer in
** the documentation and/or other materials provided with the
** distribution.
** * Neither the name of The Qt Company Ltd nor the names of its
** contributors may be used to endorse or promote products derived
** from this software without specific prior written permission.
**
**
** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
**
** $QT_END_LICENSE$
**
****************************************************************************/
#include "scriptdebugger.h"
#include "scriptbreakpointmanager.h"
#include <QtScript/QScriptEngine>
#include <QtScript/QScriptEngineAgent>
#include <QtScript/QScriptContextInfo>
#include <QtScript/QScriptValueIterator>
#include <QtCore/QTextStream>
#include <QtCore/QStack>
static QString safeValueToString(const QScriptValue &value)
{
if (value.isObject())
return QLatin1String("[object Object]");
else
return value.toString();
}
class ScriptInfo;
class ScriptBreakpointManager;
class ScriptDebuggerPrivate
: public QScriptEngineAgent
{
Q_DECLARE_PUBLIC(ScriptDebugger)
public:
enum Mode {
Run,
StepInto,
StepOver
};
ScriptDebuggerPrivate(QScriptEngine *engine);
~ScriptDebuggerPrivate();
// QScriptEngineAgent interface
void scriptLoad(qint64 id, const QString &program,
const QString &fileName, int lineNumber);
void scriptUnload(qint64 id);
void positionChange(qint64 scriptId,
int lineNumber, int columnNumber);
void functionEntry(qint64 scriptId);
void functionExit(qint64 scriptId,
const QScriptValue &returnValue);
void exceptionThrow(qint64 scriptId,
const QScriptValue &exception, bool hasHandler);
void interactive();
bool executeCommand(const QString &command, const QStringList &args);
void setMode(Mode mode);
Mode mode() const;
int frameCount() const;
void setCurrentFrameIndex(int index);
int currentFrameIndex() const;
QScriptContext *frameContext(int index) const;
QScriptContext *currentFrameContext() const;
ScriptInfo *scriptInfo(QScriptContext *context) const;
int listLineNumber() const;
void setListLineNumber(int lineNumber);
QString readLine();
void output(const QString &text);
void message(const QString &text);
void errorMessage(const QString &text);
// attributes
QTextStream *m_defaultInputStream;
QTextStream *m_defaultOutputStream;
QTextStream *m_defaultErrorStream;
QTextStream *m_inputStream;
QTextStream *m_outputStream;
QTextStream *m_errorStream;
ScriptBreakpointManager *m_bpManager;
Mode m_mode;
QMap<qint64, ScriptInfo*> m_scripts;
QMap<QScriptContext*, QStack<qint64> > m_contextProgramIds;
QString m_lastInteractiveCommand;
QString m_commandPrefix;
int m_stepDepth;
int m_currentFrameIndex;
int m_listLineNumber;
ScriptDebugger *q_ptr;
};
class ScriptInfo
{
public:
ScriptInfo(const QString &code, const QString &fileName, int lineNumber)
: m_code(code), m_fileName(fileName), m_lineNumber(lineNumber)
{ }
inline QString code() const
{ return m_code; }
inline QString fileName() const
{ return m_fileName; }
inline int lineNumber() const
{ return m_lineNumber; }
QString lineText(int lineNumber);
QMap<int, int> m_lineOffsets;
private:
int lineOffset(int lineNumber);
QString m_code;
QString m_fileName;
int m_lineNumber;
};
int ScriptInfo::lineOffset(int lineNumber)
{
QMap<int, int>::const_iterator it = m_lineOffsets.constFind(lineNumber);
if (it != m_lineOffsets.constEnd())
return it.value();
int offset;
it = m_lineOffsets.constFind(lineNumber - 1);
if (it != m_lineOffsets.constEnd()) {
offset = it.value();
offset = m_code.indexOf(QLatin1Char('\n'), offset);
if (offset != -1)
++offset;
m_lineOffsets.insert(lineNumber, offset);
} else {
int index;
it = m_lineOffsets.lowerBound(lineNumber);
if (it != m_lineOffsets.constBegin())
--it;
if (it != m_lineOffsets.constBegin()) {
index = it.key();
offset = it.value();
} else {
index = m_lineNumber;
offset = 0;
}
int j = index;
for ( ; j < lineNumber; ++j) {
m_lineOffsets.insert(j, offset);
offset = m_code.indexOf(QLatin1Char('\n'), offset);
if (offset == -1)
break;
++offset;
}
m_lineOffsets.insert(j, offset);
}
return offset;
}
QString ScriptInfo::lineText(int lineNumber)
{
int startOffset = lineOffset(lineNumber);
if (startOffset == -1)
return QString();
int endOffset = lineOffset(lineNumber + 1);
if (endOffset == -1)
return m_code.mid(startOffset);
else
return m_code.mid(startOffset, endOffset - startOffset - 1);
}
ScriptDebuggerPrivate::ScriptDebuggerPrivate(QScriptEngine *engine)
: QScriptEngineAgent(engine), m_mode(Run)
{
m_commandPrefix = QLatin1String(".");
m_bpManager = new ScriptBreakpointManager;
m_defaultInputStream = new QTextStream(stdin);
m_defaultOutputStream = new QTextStream(stdout);
m_defaultErrorStream = new QTextStream(stderr);
m_inputStream = m_defaultInputStream;
m_outputStream = m_defaultOutputStream;
m_errorStream = m_defaultErrorStream;
}
ScriptDebuggerPrivate::~ScriptDebuggerPrivate()
{
delete m_defaultInputStream;
delete m_defaultOutputStream;
delete m_defaultErrorStream;
delete m_bpManager;
qDeleteAll(m_scripts);
}
QString ScriptDebuggerPrivate::readLine()
{
return m_inputStream->readLine();
}
void ScriptDebuggerPrivate::output(const QString &text)
{
*m_outputStream << text;
}
void ScriptDebuggerPrivate::message(const QString &text)
{
*m_outputStream << text << endl;
m_outputStream->flush();
}
void ScriptDebuggerPrivate::errorMessage(const QString &text)
{
*m_errorStream << text << endl;
m_errorStream->flush();
}
void ScriptDebuggerPrivate::setMode(Mode mode)
{
m_mode = mode;
}
ScriptDebuggerPrivate::Mode ScriptDebuggerPrivate::mode() const
{
return m_mode;
}
QScriptContext *ScriptDebuggerPrivate::frameContext(int index) const
{
QScriptContext *ctx = engine()->currentContext();
for (int i = 0; i < index; ++i) {
ctx = ctx->parentContext();
if (!ctx)
break;
}
return ctx;
}
int ScriptDebuggerPrivate::currentFrameIndex() const
{
return m_currentFrameIndex;
}
void ScriptDebuggerPrivate::setCurrentFrameIndex(int index)
{
m_currentFrameIndex = index;
m_listLineNumber = -1;
}
int ScriptDebuggerPrivate::listLineNumber() const
{
return m_listLineNumber;
}
void ScriptDebuggerPrivate::setListLineNumber(int lineNumber)
{
m_listLineNumber = lineNumber;
}
QScriptContext *ScriptDebuggerPrivate::currentFrameContext() const
{
return frameContext(currentFrameIndex());
}
int ScriptDebuggerPrivate::frameCount() const
{
int count = 0;
QScriptContext *ctx = engine()->currentContext();
while (ctx) {
++count;
ctx = ctx->parentContext();
}
return count;
}
ScriptInfo *ScriptDebuggerPrivate::scriptInfo(QScriptContext *context) const
{
QStack<qint64> pids = m_contextProgramIds.value(context);
if (pids.isEmpty())
return 0;
return m_scripts.value(pids.top());
}
void ScriptDebuggerPrivate::interactive()
{
setCurrentFrameIndex(0);
QString qsdbgPrompt = QString::fromLatin1("(qsdbg) ");
QString dotPrompt = QString::fromLatin1(".... ");
QString prompt = qsdbgPrompt;
QString code;
forever {
*m_outputStream << prompt;
m_outputStream->flush();
QString line = readLine();
if (code.isEmpty() && (line.isEmpty() || line.startsWith(m_commandPrefix))) {
if (line.isEmpty())
line = m_lastInteractiveCommand;
else
m_lastInteractiveCommand = line;
QStringList parts = line.split(QLatin1Char(' '), Qt::SkipEmptyParts);
if (!parts.isEmpty()) {
QString command = parts.takeFirst().mid(1);
if (executeCommand(command, parts))
break;
}
} else {
if (line.isEmpty())
continue;
code += line;
code += QLatin1Char('\n');
if (line.trimmed().isEmpty()) {
continue;
} else if (! engine()->canEvaluate(code)) {
prompt = dotPrompt;
} else {
setMode(Run);
QScriptValue result = engine()->evaluate(code, QLatin1String("typein"));
code.clear();
prompt = qsdbgPrompt;
if (! result.isUndefined()) {
errorMessage(result.toString());
engine()->clearExceptions();
}
}
}
}
}
bool ScriptDebuggerPrivate::executeCommand(const QString &command, const QStringList &args)
{
if (command == QLatin1String("c")
|| command == QLatin1String("continue")) {
setMode(Run);
return true;
} else if (command == QLatin1String("s")
|| command == QLatin1String("step")) {
setMode(StepInto);
return true;
} else if (command == QLatin1String("n")
|| command == QLatin1String("next")) {
setMode(StepOver);
m_stepDepth = 0;
return true;
} else if (command == QLatin1String("f")
|| command == QLatin1String("frame")) {
bool ok = false;
int index = args.value(0).toInt(&ok);
if (ok) {
if (index < 0 || index >= frameCount()) {
errorMessage("No such frame.");
} else {
setCurrentFrameIndex(index);
QScriptContext *ctx = currentFrameContext();
message(QString::fromLatin1("#%0 %1").arg(index).arg(ctx->toString()));
}
}
} else if (command == QLatin1String("bt")
|| command == QLatin1String("backtrace")) {
QScriptContext *ctx = engine()->currentContext();
int index = -1;
while (ctx) {
++index;
QString line = ctx->toString();
message(QString::fromLatin1("#%0 %1").arg(index).arg(line));
ctx = ctx->parentContext();
}
} else if (command == QLatin1String("up")) {
int index = currentFrameIndex() + 1;
if (index == frameCount()) {
errorMessage(QString::fromLatin1("Initial frame selected; you cannot go up."));
} else {
setCurrentFrameIndex(index);
QScriptContext *ctx = currentFrameContext();
message(QString::fromLatin1("#%0 %1").arg(index).arg(ctx->toString()));
}
} else if (command == QLatin1String("down")) {
int index = currentFrameIndex() - 1;
if (index < 0) {
errorMessage(QString::fromLatin1("Bottom (innermost) frame selected; you cannot go down."));
} else {
setCurrentFrameIndex(index);
QScriptContext *ctx = currentFrameContext();
message(QString::fromLatin1("#%0 %1").arg(index).arg(ctx->toString()));
}
} else if (command == QLatin1String("b")
|| command == QLatin1String("break")) {
QString str = args.value(0);
int colonIndex = str.indexOf(QLatin1Char(':'));
if (colonIndex != -1) {
// filename:line form
QString fileName = str.left(colonIndex);
int lineNumber = str.mid(colonIndex+1).toInt();
int id = m_bpManager->setBreakpoint(fileName, lineNumber);
message(QString::fromLatin1("Breakpoint %0 at %1, line %2.").arg(id+1).arg(fileName).arg(lineNumber));
} else {
// function
QScriptValue fun = engine()->globalObject().property(str);
if (fun.isFunction()) {
int id = m_bpManager->setBreakpoint(fun);
message(QString::fromLatin1("Breakpoint %0 at %1().").arg(id+1).arg(str));
}
}
} else if (command == QLatin1String("d")
|| command == QLatin1String("delete")) {
int id = args.value(0).toInt() - 1;
m_bpManager->removeBreakpoint(id);
} else if (command == QLatin1String("disable")) {
int id = args.value(0).toInt() - 1;
m_bpManager->setBreakpointEnabled(id, false);
} else if (command == QLatin1String("enable")) {
int id = args.value(0).toInt() - 1;
m_bpManager->setBreakpointEnabled(id, true);
} else if (command == QLatin1String("list")) {
QScriptContext *ctx = currentFrameContext();
ScriptInfo *progInfo = scriptInfo(ctx);
if (!progInfo) {
errorMessage("No source text available for this frame.");
} else {
QScriptContextInfo ctxInfo(ctx);
bool ok;
int line = args.value(0).toInt(&ok);
if (ok) {
line = qMax(1, line - 5);
} else {
line = listLineNumber();
if (line == -1)
line = qMax(progInfo->lineNumber(), ctxInfo.lineNumber() - 5);
}
for (int i = line; i < line + 10; ++i) {
message(QString::fromLatin1("%0\t%1").arg(i).arg(progInfo->lineText(i)));
}
setListLineNumber(line + 10);
}
} else if (command == QLatin1String("info")) {
if (args.size() < 1) {
} else {
QString what = args.value(0);
if (what == QLatin1String("locals")) {
QScriptValueIterator it(currentFrameContext()->activationObject());
while (it.hasNext()) {
it.next();
QString line;
line.append(it.name());
line.append(QLatin1String(" = "));
line.append(safeValueToString(it.value()));
message(line);
}
}
}
} else if (command == QLatin1String("help")) {
message("continue - continue execution\n"
"step - step into statement\n"
"next - step over statement\n"
"list - show where you are\n"
"\n"
"break - set breakpoint\n"
"delete - remove breakpoint\n"
"disable - disable breakpoint\n"
"enable - enable breakpoint\n"
"\n"
"backtrace - show backtrace\n"
"up - one frame up\n"
"down - one frame down\n"
"frame - set frame\n"
"\n"
"info locals - show local variables");
} else {
errorMessage(QString::fromLatin1("Undefined command \"%0\". Try \"help\".")
.arg(command));
}
return false;
}
// QScriptEngineAgent interface
void ScriptDebuggerPrivate::scriptLoad(qint64 id, const QString &program,
const QString &fileName, int lineNumber)
{
ScriptInfo *info = new ScriptInfo(program, fileName, lineNumber);
m_scripts.insert(id, info);
}
void ScriptDebuggerPrivate::scriptUnload(qint64 id)
{
ScriptInfo *info = m_scripts.take(id);
delete info;
}
void ScriptDebuggerPrivate::functionEntry(qint64 scriptId)
{
if (scriptId != -1) {
QScriptContext *ctx = engine()->currentContext();
QStack<qint64> ids = m_contextProgramIds.value(ctx);
ids.push(scriptId);
m_contextProgramIds.insert(ctx, ids);
}
if (mode() == StepOver)
++m_stepDepth;
}
void ScriptDebuggerPrivate::functionExit(qint64 scriptId,
const QScriptValue &/*returnValue*/)
{
if (scriptId != -1) {
QScriptContext *ctx = engine()->currentContext();
QStack<qint64> ids = m_contextProgramIds.value(ctx);
Q_ASSERT(!ids.isEmpty());
Q_ASSERT(ids.top() == scriptId);
ids.pop();
m_contextProgramIds.insert(ctx, ids);
}
if (mode() == StepOver)
--m_stepDepth;
}
void ScriptDebuggerPrivate::positionChange(qint64 scriptId,
int lineNumber, int /*columnNumber*/)
{
ScriptInfo *info = 0;
bool enterInteractiveMode = false;
if (m_bpManager->hasBreakpoints()) {
// check if we hit a breakpoint
info = m_scripts.value(scriptId);
QScriptContext *ctx = engine()->currentContext();
QScriptContextInfo ctxInfo(ctx);
QScriptValue callee = ctx->callee();
// try fileName:lineNumber
int bpid = m_bpManager->findBreakpoint(info->fileName(), lineNumber);
if ((bpid != -1) && m_bpManager->isBreakpointEnabled(bpid)) {
message(QString::fromLatin1("Breakpoint %0 at %1:%2")
.arg(bpid + 1).arg(info->fileName()).arg(lineNumber));
if (m_bpManager->isBreakpointSingleShot(bpid))
m_bpManager->removeBreakpoint(bpid);
}
if (bpid == -1) {
// try function
bpid = m_bpManager->findBreakpoint(callee);
if ((bpid != -1) && m_bpManager->isBreakpointEnabled(bpid)) {
message(QString::fromLatin1("Breakpoint %0, %1()")
.arg(bpid + 1).arg(ctxInfo.functionName()));
if (m_bpManager->isBreakpointSingleShot(bpid))
m_bpManager->removeBreakpoint(bpid);
}
}
if ((bpid == -1) && !ctxInfo.functionName().isEmpty()) {
// try functionName:fileName
bpid = m_bpManager->findBreakpoint(ctxInfo.functionName(), ctxInfo.fileName());
if ((bpid != -1) && m_bpManager->isBreakpointEnabled(bpid)) {
message(QString::fromLatin1("Breakpoint %0, %1():%2").arg(bpid + 1)
.arg(ctxInfo.functionName()).arg(ctxInfo.fileName()));
if (m_bpManager->isBreakpointSingleShot(bpid))
m_bpManager->removeBreakpoint(bpid);
}
}
enterInteractiveMode = (bpid != -1);
}
switch (mode()) {
case Run:
break;
case StepInto:
enterInteractiveMode = true;
break;
case StepOver:
enterInteractiveMode = enterInteractiveMode || (m_stepDepth <= 0);
break;
}
if (enterInteractiveMode) {
if (!info)
info = m_scripts.value(scriptId);
Q_ASSERT(info);
message(QString::fromLatin1("%0\t%1").arg(lineNumber).arg(info->lineText(lineNumber)));
interactive();
}
}
void ScriptDebuggerPrivate::exceptionThrow(qint64 /*scriptId*/,
const QScriptValue &exception,
bool hasHandler)
{
if (!hasHandler) {
errorMessage(QString::fromLatin1("uncaught exception: %0").arg(exception.toString()));
QScriptContext *ctx = engine()->currentContext();
int lineNumber = QScriptContextInfo(ctx).lineNumber();
ScriptInfo *info = scriptInfo(ctx);
QString lineText = info ? info->lineText(lineNumber) : QString("(no source text available)");
message(QString::fromLatin1("%0\t%1").arg(lineNumber).arg(lineText));
interactive();
}
}
ScriptDebugger::ScriptDebugger(QScriptEngine *engine)
: d_ptr(new ScriptDebuggerPrivate(engine))
{
d_ptr->q_ptr = this;
engine->setAgent(d_ptr);
}
ScriptDebugger::ScriptDebugger(QScriptEngine *engine, ScriptDebuggerPrivate &dd)
: d_ptr(&dd)
{
d_ptr->q_ptr = this;
engine->setAgent(d_ptr);
}
ScriptDebugger::~ScriptDebugger()
{
delete d_ptr;
d_ptr = 0;
}
void ScriptDebugger::breakAtNextStatement()
{
Q_D(ScriptDebugger);
d->setMode(ScriptDebuggerPrivate::StepInto);
}
void ScriptDebugger::setBreakpoint(const QString &fileName, int lineNumber)
{
Q_D(ScriptDebugger);
d->m_bpManager->setBreakpoint(fileName, lineNumber);
}
void ScriptDebugger::setBreakpoint(const QString &functionName, const QString &fileName)
{
Q_D(ScriptDebugger);
d->m_bpManager->setBreakpoint(functionName, fileName);
}
void ScriptDebugger::setBreakpoint(const QScriptValue &function)
{
Q_D(ScriptDebugger);
d->m_bpManager->setBreakpoint(function);
}
QTextStream *ScriptDebugger::inputStream() const
{
Q_D(const ScriptDebugger);
return d->m_inputStream;
}
void ScriptDebugger::setInputStream(QTextStream *inputStream)
{
Q_D(ScriptDebugger);
d->m_inputStream = inputStream;
}
QTextStream *ScriptDebugger::outputStream() const
{
Q_D(const ScriptDebugger);
return d->m_outputStream;
}
void ScriptDebugger::setOutputStream(QTextStream *outputStream)
{
Q_D(ScriptDebugger);
d->m_outputStream = outputStream;
}
QTextStream *ScriptDebugger::errorStream() const
{
Q_D(const ScriptDebugger);
return d->m_errorStream;
}
void ScriptDebugger::setErrorStream(QTextStream *errorStream)
{
Q_D(ScriptDebugger);
d->m_errorStream = errorStream;
}