/************************************************************************** ** ** This file is part of Qt Creator ** ** Copyright (c) 2010 Nokia Corporation and/or its subsidiary(-ies). ** ** Contact: Nokia Corporation (qt-info@nokia.com) ** ** Commercial Usage ** ** Licensees holding valid Qt Commercial licenses may use this file in ** accordance with the Qt Commercial License Agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and Nokia. ** ** GNU Lesser General Public License Usage ** ** Alternatively, 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. ** ** If you are unsure which license is appropriate for your use, please ** contact the sales department at http://qt.nokia.com/contact. ** **************************************************************************/ #define QT_NO_CAST_FROM_ASCII #include "scriptengine.h" #include "breakhandler.h" #include "debuggerconstants.h" #include "debuggerdialogs.h" #include "debuggerstringutils.h" #include "moduleshandler.h" #include "registerhandler.h" #include "stackhandler.h" #include "watchhandler.h" #include "watchutils.h" #include <utils/qtcassert.h> #include <texteditor/itexteditor.h> #include <coreplugin/ifile.h> #include <coreplugin/scriptmanager/scriptmanager.h> #include <coreplugin/icore.h> #include <QtCore/QDateTime> #include <QtCore/QDebug> #include <QtCore/QDir> #include <QtCore/QFileInfo> #include <QtCore/QTimer> #include <QtGui/QApplication> #include <QtGui/QToolTip> #include <QtScript/QScriptContext> #include <QtScript/QScriptClassPropertyIterator> #include <QtScript/QScriptContextInfo> #include <QtScript/QScriptEngine> #include <QtScript/QScriptEngineAgent> #include <QtScript/QScriptValue> #include <QtScript/QScriptValueIterator> #define DEBUG_SCRIPT 1 #if DEBUG_SCRIPT # define SDEBUG(s) qDebug() << s #else # define SDEBUG(s) #endif # define XSDEBUG(s) qDebug() << s namespace Debugger { namespace Internal { /////////////////////////////////////////////////////////////////////// // // ScriptEngine // /////////////////////////////////////////////////////////////////////// class ScriptAgent : public QScriptEngineAgent { public: ScriptAgent(ScriptEngine *debugger, QScriptEngine *script); ~ScriptAgent() {} void contextPop(); void contextPush(); void exceptionCatch(qint64 scriptId, const QScriptValue &exception); void exceptionThrow(qint64 scriptId, const QScriptValue & exception, bool hasHandler); void functionEntry(qint64 scriptId); void functionExit(qint64 scriptId, const QScriptValue &returnValue); void positionChange(qint64 scriptId, int lineNumber, int columnNumber); void scriptLoad(qint64 id, const QString &program, const QString &fileName, int baseLineNumber); void scriptUnload(qint64 id); private: void maybeBreakNow(bool byFunction); ScriptEngine *q; }; ScriptAgent::ScriptAgent(ScriptEngine *debugger, QScriptEngine *script) : QScriptEngineAgent(script), q(debugger) {} void ScriptAgent::contextPop() { SDEBUG("ScriptAgent::contextPop: "); } void ScriptAgent::contextPush() { SDEBUG("ScriptAgent::contextPush: "); } void ScriptAgent::exceptionCatch(qint64 scriptId, const QScriptValue & exception) { Q_UNUSED(scriptId) Q_UNUSED(exception) const QString msg = QString::fromLatin1("An exception was caught on %1: '%2'"). arg(scriptId).arg(exception.toString()); SDEBUG(msg); q->showMessage(msg, LogMisc); } void ScriptAgent::exceptionThrow(qint64 scriptId, const QScriptValue &exception, bool hasHandler) { Q_UNUSED(scriptId) Q_UNUSED(exception) Q_UNUSED(hasHandler) const QString msg = QString::fromLatin1("An exception occurred on %1: '%2'"). arg(scriptId).arg(exception.toString()); SDEBUG(msg); q->showMessage(msg, LogMisc); } void ScriptAgent::functionEntry(qint64 scriptId) { Q_UNUSED(scriptId) q->showMessage(QString::fromLatin1("Function entry occurred on %1").arg(scriptId), LogMisc); q->checkForBreakCondition(true); } void ScriptAgent::functionExit(qint64 scriptId, const QScriptValue &returnValue) { Q_UNUSED(scriptId) Q_UNUSED(returnValue) const QString msg = QString::fromLatin1("Function exit occurred on %1: '%2'").arg(scriptId).arg(returnValue.toString()); SDEBUG(msg); q->showMessage(msg, LogMisc); } void ScriptAgent::positionChange(qint64 scriptId, int lineNumber, int columnNumber) { SDEBUG("ScriptAgent::position: " << lineNumber); Q_UNUSED(scriptId) Q_UNUSED(lineNumber) Q_UNUSED(columnNumber) q->checkForBreakCondition(false); } void ScriptAgent::scriptLoad(qint64 scriptId, const QString &program, const QString &fileName, int baseLineNumber) { Q_UNUSED(scriptId) Q_UNUSED(program) Q_UNUSED(fileName) Q_UNUSED(baseLineNumber) q->showMessage(QString::fromLatin1("Loaded: %1 id: %2") .arg(fileName).arg(scriptId), LogMisc); } void ScriptAgent::scriptUnload(qint64 scriptId) { Q_UNUSED(scriptId) SDEBUG("ScriptAgent::scriptUnload: " << scriptId); } /////////////////////////////////////////////////////////////////////// // // ScriptEngine // /////////////////////////////////////////////////////////////////////// ScriptEngine::ScriptEngine(const DebuggerStartParameters &startParameters) : DebuggerEngine(startParameters) { } ScriptEngine::~ScriptEngine() { } void ScriptEngine::executeDebuggerCommand(const QString &command) { Q_UNUSED(command) XSDEBUG("FIXME: ScriptEngine::executeDebuggerCommand()"); } void ScriptEngine::shutdown() { exitDebugger(); } void ScriptEngine::exitDebugger() { if (state() == DebuggerNotReady) return; SDEBUG("ScriptEngine::exitDebugger()"); m_stopped = false; m_stopOnNextLine = false; if (m_scriptEngine->isEvaluating()) m_scriptEngine->abortEvaluation(); setState(InferiorShuttingDown); setState(InferiorShutDown); setState(EngineShuttingDown); m_scriptEngine->setAgent(0); setState(DebuggerNotReady); } void ScriptEngine::setupEngine() { QTC_ASSERT(state() == EngineStarting, qDebug() << state()); showMessage(_("STARTING SCRIPT DEBUGGER"), LogMisc); if (m_scriptEngine.isNull()) m_scriptEngine = Core::ICore::instance()->scriptManager()->scriptEngine(); if (!m_scriptAgent) m_scriptAgent.reset(new ScriptAgent(this, m_scriptEngine.data())); m_scriptEngine->setAgent(m_scriptAgent.data()); /* Keep the gui alive (have the engine call processEvents() while the script * is run in the foreground). */ m_scriptEngine->setProcessEventsInterval(1 /*ms*/); m_stopped = false; m_stopOnNextLine = false; m_scriptEngine->abortEvaluation(); notifyEngineStartOk(); } void ScriptEngine::setupInferior() { QTC_ASSERT(state() == InferiorSettingUp, qDebug() << state()); m_scriptFileName = QFileInfo(startParameters().executable).absoluteFilePath(); showMessage(_("SCRIPT FILE: ") + m_scriptFileName); QFile scriptFile(m_scriptFileName); if (!scriptFile.open(QIODevice::ReadOnly|QIODevice::Text)) { showMessage(QString::fromLatin1("Cannot open %1: %2"). arg(m_scriptFileName, scriptFile.errorString()), LogError); notifyEngineStartFailed(); return; } QTextStream stream(&scriptFile); m_scriptContents = stream.readAll(); scriptFile.close(); attemptBreakpointSynchronization(); notifyInferiorSetupOk(); } void ScriptEngine::continueInferior() { SDEBUG("ScriptEngine::continueInferior()"); m_stopped = false; m_stopOnNextLine = false; } static const char *qtExtensionsC[] = { "qt.core", "qt.gui", "qt.xml", "qt.svg", "qt.network", "qt.sql", "qt.opengl", "qt.webkit", "qt.xmlpatterns", "qt.uitools" }; bool ScriptEngine::importExtensions() { SDEBUG("ScriptEngine::importExtensions()"); QStringList extensions; const int extCount = sizeof(qtExtensionsC)/sizeof(const char *); for (int e = 0; e < extCount; e++) extensions.append(QLatin1String(qtExtensionsC[e])); if (m_scriptEngine->importedExtensions().contains(extensions.front())) return true; QDir dir(QLatin1String("/home/apoenitz/dev/qtscriptgenerator")); if (!dir.cd(QLatin1String("plugins"))) { fprintf(stderr, "plugins folder does not exist -- did you build the bindings?\n"); return false; } QStringList paths = qApp->libraryPaths(); paths << dir.absolutePath(); qApp->setLibraryPaths(paths); QStringList failExtensions; foreach (const QString &ext, extensions) { QScriptValue ret = m_scriptEngine->importExtension(ext); if (ret.isError()) failExtensions.append(ext); } if (!failExtensions.isEmpty()) { if (failExtensions.size() == extensions.size()) { qWarning("Failed to import Qt bindings!\n" "Plugins directory searched: %s/script\n" "Make sure that the bindings have been built, " "and that this executable and the plugins are " "using compatible Qt libraries.", qPrintable(dir.absolutePath())); } else { qWarning("Failed to import some Qt bindings: %s\n" "Plugins directory searched: %s/script\n" "Make sure that the bindings have been built, " "and that this executable and the plugins are " "using compatible Qt libraries.", qPrintable(failExtensions.join(QLatin1String(", "))), qPrintable(dir.absolutePath())); } } return failExtensions.isEmpty(); } void ScriptEngine::runEngine() { QTC_ASSERT(state() == InferiorSetupOk, qDebug() << state()); setState(InferiorRunningRequested); showStatusMessage(tr("Running requested..."), 5000); showMessage(QLatin1String("Running: ") + m_scriptFileName, LogMisc); SDEBUG("ScriptEngine::runEngine()"); importExtensions(); setState(InferiorRunning); const QScriptValue result = m_scriptEngine->evaluate(m_scriptContents, m_scriptFileName); setState(InferiorStopping); setState(InferiorStopped); QString msg; if (m_scriptEngine->hasUncaughtException()) { msg = QString::fromLatin1("An exception occurred during execution at line: %1\n%2\n") .arg(m_scriptEngine->uncaughtExceptionLineNumber()) .arg(m_scriptEngine->uncaughtException().toString()); msg += m_scriptEngine->uncaughtExceptionBacktrace() .join(QString(QLatin1Char('\n'))); } else { msg = QString::fromLatin1("Evaluation returns '%1'") .arg(result.toString()); } showMessage(msg, LogMisc); exitDebugger(); } void ScriptEngine::interruptInferior() { m_stopped = false; m_stopOnNextLine = true; XSDEBUG("ScriptEngine::interruptInferior()"); } void ScriptEngine::executeStep() { //SDEBUG("ScriptEngine::stepExec()"); m_stopped = false; m_stopOnNextLine = true; } void ScriptEngine::executeStepI() { //SDEBUG("ScriptEngine::stepIExec()"); m_stopped = false; m_stopOnNextLine = true; } void ScriptEngine::executeStepOut() { //SDEBUG("ScriptEngine::stepOutExec()"); m_stopped = false; m_stopOnNextLine = true; } void ScriptEngine::executeNext() { //SDEBUG("ScriptEngine::nextExec()"); m_stopped = false; m_stopOnNextLine = true; } void ScriptEngine::executeNextI() { //SDEBUG("ScriptEngine::nextIExec()"); m_stopped = false; m_stopOnNextLine = true; } void ScriptEngine::executeRunToLine(const QString &fileName, int lineNumber) { Q_UNUSED(fileName) Q_UNUSED(lineNumber) SDEBUG("FIXME: ScriptEngine::runToLineExec()"); } void ScriptEngine::executeRunToFunction(const QString &functionName) { Q_UNUSED(functionName) XSDEBUG("FIXME: ScriptEngine::runToFunctionExec()"); } void ScriptEngine::executeJumpToLine(const QString &fileName, int lineNumber) { Q_UNUSED(fileName) Q_UNUSED(lineNumber) XSDEBUG("FIXME: ScriptEngine::jumpToLineExec()"); } void ScriptEngine::activateFrame(int index) { Q_UNUSED(index) } void ScriptEngine::selectThread(int index) { Q_UNUSED(index) } void ScriptEngine::attemptBreakpointSynchronization() { BreakHandler *handler = breakHandler(); bool updateNeeded = false; for (int index = 0; index != handler->size(); ++index) { BreakpointData *data = handler->at(index); if (data->pending) { data->pending = false; // FIXME updateNeeded = true; } if (data->bpNumber.isEmpty()) { data->bpNumber = QByteArray::number(index + 1); updateNeeded = true; } if (!data->fileName.isEmpty() && data->markerFileName().isEmpty()) { data->setMarkerFileName(data->fileName); data->setMarkerLineNumber(data->lineNumber.toInt()); updateNeeded = true; } } if (updateNeeded) handler->updateMarkers(); } void ScriptEngine::loadSymbols(const QString &moduleName) { Q_UNUSED(moduleName) } void ScriptEngine::loadAllSymbols() { } void ScriptEngine::reloadModules() { } void ScriptEngine::requestModuleSymbols(const QString & /*moduleName*/) { } ////////////////////////////////////////////////////////////////////// // // Tooltip specific stuff // ////////////////////////////////////////////////////////////////////// static WatchData m_toolTip; static QPoint m_toolTipPos; static QHash<QString, WatchData> m_toolTipCache; void ScriptEngine::setToolTipExpression(const QPoint &mousePos, TextEditor::ITextEditor *editor, int cursorPos) { Q_UNUSED(mousePos) Q_UNUSED(editor) Q_UNUSED(cursorPos) if (state() != InferiorStopped) { //SDEBUG("SUPPRESSING DEBUGGER TOOLTIP, INFERIOR NOT STOPPED"); return; } // Check mime type and get expression (borrowing some C++ - functions) const QString javaScriptMimeType = QLatin1String("application/javascript"); if (!editor->file() || editor->file()->mimeType() != javaScriptMimeType) return; int line; int column; QString exp = cppExpressionAt(editor, cursorPos, &line, &column); /* if (m_toolTipCache.contains(exp)) { const WatchData & data = m_toolTipCache[exp]; q->watchHandler()->removeChildren(data.iname); insertData(data); return; } */ QToolTip::hideText(); if (exp.isEmpty() || exp.startsWith(QLatin1Char('#'))) { QToolTip::hideText(); return; } if (!hasLetterOrNumber(exp)) { QToolTip::showText(m_toolTipPos, tr("'%1' contains no identifier").arg(exp)); return; } if (exp.startsWith(QLatin1Char('"')) && exp.endsWith(QLatin1Char('"'))) { QToolTip::showText(m_toolTipPos, tr("String literal %1").arg(exp)); return; } if (exp.startsWith(QLatin1String("++")) || exp.startsWith(QLatin1String("--"))) exp.remove(0, 2); if (exp.endsWith(QLatin1String("++")) || exp.endsWith(QLatin1String("--"))) exp.remove(0, 2); if (exp.startsWith(QLatin1Char('<')) || exp.startsWith(QLatin1Char('['))) return; if (hasSideEffects(exp)) { QToolTip::showText(m_toolTipPos, tr("Cowardly refusing to evaluate expression '%1' " "with potential side effects").arg(exp)); return; } #if 0 //if (m_manager->status() != InferiorStopped) // return; // FIXME: 'exp' can contain illegal characters m_toolTip = WatchData(); m_toolTip.exp = exp; m_toolTip.name = exp; m_toolTip.iname = tooltipIName; insertData(m_toolTip); #endif } ////////////////////////////////////////////////////////////////////// // // Watch specific stuff // ////////////////////////////////////////////////////////////////////// void ScriptEngine::assignValueInDebugger(const QString &expression, const QString &value) { SDEBUG("ASSIGNING: " << (expression + QLatin1Char('=') + value)); m_scriptEngine->evaluate(expression + QLatin1Char('=') + value); updateLocals(); } static BreakpointData *findBreakPointByFunction(BreakHandler *handler, const QString &functionName) { const int count = handler->size(); for (int b = 0; b < count; b++) { BreakpointData *data = handler->at(b); if (data->funcName == functionName) return data; } return 0; } static BreakpointData *findBreakPointByFileName(BreakHandler *handler, int lineNumber, const QString &fileName) { const int count = handler->size(); for (int b = 0; b < count; b++) { BreakpointData *data = handler->at(b); if (lineNumber == data->lineNumber.toInt() && fileName == data->fileName) return data; } return 0; } bool ScriptEngine::checkForBreakCondition(bool byFunction) { const QScriptContext *context = m_scriptEngine->currentContext(); const QScriptContextInfo info(context); // Update breakpoints const QString functionName = info.functionName(); const QString fileName = info.fileName(); const int lineNumber = byFunction ? info.functionStartLineNumber() : info.lineNumber(); SDEBUG("checkForBreakCondition" << byFunction << functionName << lineNumber << fileName); if (m_stopOnNextLine) { // Interrupt inferior m_stopOnNextLine = false; } else { if (byFunction && functionName.isEmpty()) return false; BreakpointData *data = byFunction ? findBreakPointByFunction(breakHandler(), functionName) : findBreakPointByFileName(breakHandler(), lineNumber, fileName); if (!data) return false; // Skip disabled breakpoint. if (!data->enabled) return false; // We just run into a breakpoint. //SDEBUG("RESOLVING BREAKPOINT AT " << fileName << lineNumber); data->bpLineNumber = QByteArray::number(lineNumber); data->bpFileName = fileName; data->bpFuncName = functionName; data->setMarkerLineNumber(lineNumber); data->setMarkerFileName(fileName); data->pending = false; data->updateMarker(); } setState(InferiorStopping); setState(InferiorStopped); SDEBUG("Stopped at " << lineNumber << fileName); showStatusMessage(tr("Stopped at %1:%2.").arg(fileName).arg(lineNumber), 5000); StackFrame frame; frame.file = fileName; frame.line = lineNumber; gotoLocation(frame, true); updateLocals(); return true; } void ScriptEngine::updateLocals() { QScriptContext *context = m_scriptEngine->currentContext(); watchHandler()->beginCycle(); //SDEBUG("UPDATE LOCALS"); // // Build stack // QList<StackFrame> stackFrames; int i = 0; for (QScriptContext *c = context; c; c = c->parentContext(), ++i) { const QScriptContextInfo info(c); StackFrame frame; frame.level = i; frame.file = info.fileName(); frame.function = info.functionName(); frame.from = QString::number(info.functionStartLineNumber()); frame.to = QString::number(info.functionEndLineNumber()); frame.line = info.lineNumber(); if (frame.function.isEmpty()) frame.function = QLatin1String("<global scope>"); //frame.address = ...; stackFrames.append(frame); } stackHandler()->setFrames(stackFrames); // // Build locals, deactivate agent meanwhile. // m_scriptEngine->setAgent(0); WatchData data; data.iname = "local"; data.name = QString::fromLatin1(data.iname); data.scriptValue = context->activationObject(); watchHandler()->beginCycle(); updateSubItem(data); watchHandler()->endCycle(); // FIXME: Use an extra thread. This here is evil m_stopped = true; showStatusMessage(tr("Stopped."), 5000); while (m_stopped) { //SDEBUG("LOOPING"); QApplication::processEvents(); } setState(InferiorRunningRequested); setState(InferiorRunning); // Clear any exceptions occurred during locals evaluation. m_scriptEngine->clearExceptions(); m_scriptEngine->setAgent(m_scriptAgent.data()); SDEBUG("Continuing"); } void ScriptEngine::updateWatchData(const WatchData &data) { updateSubItem(data); } static inline QString msgDebugInsert(const WatchData &d0, const QList<WatchData>& children) { QString rc; QTextStream str(&rc); str << "INSERTING " << d0.toString() << '\n'; foreach(const WatchData &c, children) str << " " << c.toString() << '\n'; return rc; } void ScriptEngine::updateSubItem(const WatchData &data0) { WatchData data = data0; QList<WatchData> children; SDEBUG("\nUPDATE SUBITEM: " << data.toString() << data.scriptValue.toString()); QTC_ASSERT(data.isValid(), return); if (data.isTypeNeeded() || data.isValueNeeded()) { const QScriptValue &ob = data.scriptValue; if (ob.isArray()) { data.setType(QLatin1String("Array"), false); data.setValue(QString(QLatin1Char(' '))); } else if (ob.isBool()) { data.setType(QLatin1String("Bool"), false); data.setValue(ob.toBool() ? QLatin1String("true") : QLatin1String("false")); data.setHasChildren(false); } else if (ob.isDate()) { data.setType(QLatin1String("Date"), false); data.setValue(ob.toDateTime().toString()); data.setHasChildren(false); } else if (ob.isError()) { data.setType(QLatin1String("Error"), false); data.setValue(QString(QLatin1Char(' '))); } else if (ob.isFunction()) { data.setType(QLatin1String("Function"), false); data.setValue(QString(QLatin1Char(' '))); } else if (ob.isNull()) { const QString nullValue = QLatin1String("<null>"); data.setType(nullValue, false); data.setValue(nullValue); } else if (ob.isNumber()) { data.setType(QLatin1String("Number"), false); data.setValue(QString::number(ob.toNumber())); data.setHasChildren(false); } else if (ob.isObject()) { data.setType(QLatin1String("Object"), false); data.setValue(QString(QLatin1Char(' '))); } else if (ob.isQMetaObject()) { data.setType(QLatin1String("QMetaObject"), false); data.setValue(QString(QLatin1Char(' '))); } else if (ob.isQObject()) { data.setType(QLatin1String("QObject"), false); data.setValue(QString(QLatin1Char(' '))); } else if (ob.isRegExp()) { data.setType(QLatin1String("RegExp"), false); data.setValue(ob.toRegExp().pattern()); } else if (ob.isString()) { data.setType(QLatin1String("String"), false); data.setValue(ob.toString()); } else if (ob.isVariant()) { data.setType(QLatin1String("Variant"), false); data.setValue(QString(QLatin1Char(' '))); } else if (ob.isUndefined()) { data.setType(QLatin1String("<undefined>"), false); data.setValue(QLatin1String("<unknown>")); data.setHasChildren(false); } else { const QString unknown = QLatin1String("<unknown>"); data.setType(unknown, false); data.setValue(unknown); data.setHasChildren(false); } } if (data.isChildrenNeeded()) { QScriptValueIterator it(data.scriptValue); while (it.hasNext()) { it.next(); WatchData data1; data1.iname = data.iname + '.' + it.name().toLatin1(); data1.exp = it.name().toLatin1(); data1.name = it.name(); data1.scriptValue = it.value(); if (watchHandler()->isExpandedIName(data1.iname)) { data1.setChildrenNeeded(); } else { data1.setChildrenUnneeded(); } children.push_back(data1); } data.setHasChildren(!children.isEmpty()); data.setChildrenUnneeded(); } if (data.isHasChildrenNeeded()) { QScriptValueIterator it(data.scriptValue); data.setHasChildren(it.hasNext()); } SDEBUG(msgDebugInsert(data, children)); watchHandler()->insertData(data); if (!children.isEmpty()) watchHandler()->insertBulkData(children); } DebuggerEngine *createScriptEngine(const DebuggerStartParameters &sp) { return new ScriptEngine(sp); } } // namespace Internal } // namespace Debugger