Qt Reference Documentation

scriptdebugger.cpp Example File

script/qsdbg/scriptdebugger.cpp
 /****************************************************************************
 **
 ** Copyright (C) 2015 The Qt Company Ltd.
 ** Contact: http://www.qt.io/licensing/
 **
 ** This file is part of the examples of the Qt Toolkit.
 **
 ** $QT_BEGIN_LICENSE:BSD$
 ** 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);
         --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(' '), QString::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;
 }