/**************************************************************************** ** ** Copyright (C) 2016 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of Qt Creator. ** ** 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 General Public License Usage ** Alternatively, this file may be used under the terms of the GNU ** General Public License version 3 as published by the Free Software ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT ** 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-3.0.html. ** ****************************************************************************/ #include "qmlengine.h" #include "interactiveinterpreter.h" #include "qmlinspectoragent.h" #include "qmlv8debuggerclientconstants.h" #include "qmlengineutils.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define DEBUG_QML 0 #if DEBUG_QML # define SDEBUG(s) qDebug() << s #else # define SDEBUG(s) #endif # define XSDEBUG(s) qDebug() << s #define CB(callback) [this](const QVariantMap &r) { callback(r); } #define CHECK_STATE(s) do { checkState(s, __FILE__, __LINE__); } while (0) using namespace Core; using namespace ProjectExplorer; using namespace QmlDebug; using namespace QmlJS; using namespace TextEditor; using namespace Utils; namespace Debugger { namespace Internal { enum Exceptions { NoExceptions, UncaughtExceptions, AllExceptions }; enum StepAction { Continue, StepIn, StepOut, Next }; struct QmlV8ObjectData { int handle = -1; int expectedProperties = -1; QString name; QString type; QVariant value; QVariantList properties; bool hasChildren() const { return expectedProperties > 0 || !properties.isEmpty(); } }; using QmlCallback = std::function; struct LookupData { QString iname; QString name; QString exp; }; using LookupItems = QHash; // id -> (iname, exp) static void setWatchItemHasChildren(WatchItem *item, bool hasChildren) { item->setHasChildren(hasChildren); item->valueEditable = !hasChildren; } class QmlEnginePrivate : public QmlDebugClient { public: QmlEnginePrivate(QmlEngine *engine_, QmlDebugConnection *connection) : QmlDebugClient("V8Debugger", connection), engine(engine_), inspectorAgent(engine, connection) {} void messageReceived(const QByteArray &data) override; void stateChanged(State state) override; void continueDebugging(StepAction stepAction); void evaluate(const QString expr, qint64 context, const QmlCallback &cb); void lookup(const LookupItems &items); void backtrace(); void updateLocals(); void scope(int number, int frameNumber = -1); void scripts(int types = 4, const QList ids = QList(), bool includeSource = false, const QVariant filter = QVariant()); void setBreakpoint(const QString type, const QString target, bool enabled = true,int line = 0, int column = 0, const QString condition = QString(), int ignoreCount = -1); void clearBreakpoint(const Breakpoint &bp); bool canChangeBreakpoint() const; void changeBreakpoint(const Breakpoint &bp, bool enabled); void setExceptionBreak(Exceptions type, bool enabled = false); void flushSendBuffer(); void handleBacktrace(const QVariantMap &response); void handleLookup(const QVariantMap &response); void handleExecuteDebuggerCommand(const QVariantMap &response); void handleEvaluateExpression(const QVariantMap &response, const QString &iname, const QString &expr); void handleFrame(const QVariantMap &response); void handleScope(const QVariantMap &response); void handleVersion(const QVariantMap &response); StackFrame extractStackFrame(const QVariant &bodyVal); bool canEvaluateScript(const QString &script); void updateScriptSource(const QString &fileName, int lineOffset, int columnOffset, const QString &source); void runCommand(const DebuggerCommand &command, const QmlCallback &cb = QmlCallback()); void runDirectCommand(const QString &type, const QByteArray &msg = QByteArray()); void clearRefs() { refVals.clear(); } void memorizeRefs(const QVariant &refs); QmlV8ObjectData extractData(const QVariant &data) const; void insertSubItems(WatchItem *parent, const QVariantList &properties); void checkForFinishedUpdate(); ConsoleItem *constructLogItemTree(const QmlV8ObjectData &objectData); public: QHash refVals; // The mapping of target object handles to retrieved values. int sequence = -1; QmlEngine *engine; QHash breakpointsSync; QList breakpointsTemp; LookupItems currentlyLookingUp; // Id -> inames //Cache QList currentFrameScopes; QHash stackIndexLookup; StepAction previousStepAction = Continue; QList sendBuffer; QHash sourceDocuments; QHash > sourceEditors; InteractiveInterpreter interpreter; ApplicationLauncher applicationLauncher; QmlInspectorAgent inspectorAgent; QList queryIds; bool retryOnConnectFail = false; bool automaticConnect = false; bool unpausedEvaluate = false; bool contextEvaluate = false; bool supportChangeBreakpoint = false; QTimer connectionTimer; QmlDebug::QDebugMessageClient *msgClient = nullptr; QHash callbackForToken; QMetaObject::Connection startupMessageFilterConnection; private: ConsoleItem *constructLogItemTree(const QmlV8ObjectData &objectData, QList &seenHandles); void constructChildLogItems(ConsoleItem *item, const QmlV8ObjectData &objectData, QList &seenHandles); }; static void updateDocument(IDocument *document, const QTextDocument *textDocument) { if (auto baseTextDocument = qobject_cast(document)) baseTextDocument->document()->setPlainText(textDocument->toPlainText()); } /////////////////////////////////////////////////////////////////////// // // QmlEngine // /////////////////////////////////////////////////////////////////////// QmlEngine::QmlEngine() : d(new QmlEnginePrivate(this, new QmlDebugConnection(this))) { setObjectName("QmlEngine"); setDebuggerName("QML"); QmlDebugConnection *connection = d->connection(); connect(stackHandler(), &StackHandler::stackChanged, this, &QmlEngine::updateCurrentContext); connect(stackHandler(), &StackHandler::currentIndexChanged, this, &QmlEngine::updateCurrentContext); connect(&d->applicationLauncher, &ApplicationLauncher::processExited, this, &QmlEngine::disconnected); connect(&d->applicationLauncher, &ApplicationLauncher::appendMessage, this, &QmlEngine::appMessage); connect(&d->applicationLauncher, &ApplicationLauncher::processStarted, this, &QmlEngine::handleLauncherStarted); debuggerConsole()->populateFileFinder(); debuggerConsole()->setScriptEvaluator([this](const QString &expr) { executeDebuggerCommand(expr); }); d->connectionTimer.setInterval(4000); d->connectionTimer.setSingleShot(true); connect(&d->connectionTimer, &QTimer::timeout, this, &QmlEngine::checkConnectionState); connect(connection, &QmlDebugConnection::logStateChange, this, &QmlEngine::showConnectionStateMessage); connect(connection, &QmlDebugConnection::logError, this, [this](const QString &error) { showMessage("QML Debugger: " + error, LogWarning); }); connect(connection, &QmlDebugConnection::connectionFailed, this, &QmlEngine::connectionFailed); connect(connection, &QmlDebugConnection::connected, &d->connectionTimer, &QTimer::stop); connect(connection, &QmlDebugConnection::connected, this, &QmlEngine::connectionEstablished); connect(connection, &QmlDebugConnection::disconnected, this, &QmlEngine::disconnected); d->msgClient = new QDebugMessageClient(connection); connect(d->msgClient, &QDebugMessageClient::newState, this, [this](QmlDebugClient::State state) { logServiceStateChange(d->msgClient->name(), d->msgClient->serviceVersion(), state); }); connect(d->msgClient, &QDebugMessageClient::message, this, &appendDebugOutput); } QmlEngine::~QmlEngine() { QObject::disconnect(d->startupMessageFilterConnection); QSet documentsToClose; QHash >::iterator iter; for (iter = d->sourceEditors.begin(); iter != d->sourceEditors.end(); ++iter) { QWeakPointer textEditPtr = iter.value(); if (textEditPtr) documentsToClose << textEditPtr.data()->document(); } EditorManager::closeDocuments(documentsToClose.toList()); delete d; } void QmlEngine::setState(DebuggerState state, bool forced) { DebuggerEngine::setState(state, forced); updateCurrentContext(); } void QmlEngine::handleLauncherStarted() { // FIXME: The QmlEngine never calls notifyInferiorPid() triggering the // raising, so do it here manually for now. ProcessHandle(inferiorPid()).activate(); tryToConnect(); } void QmlEngine::appMessage(const QString &msg, Utils::OutputFormat /* format */) { showMessage(msg, AppOutput); // FIXME: Redirect to RunControl } void QmlEngine::connectionEstablished() { connect(inspectorView(), &WatchTreeView::currentIndexChanged, this, &QmlEngine::updateCurrentContext); if (state() == EngineRunRequested) notifyEngineRunAndInferiorRunOk(); } void QmlEngine::tryToConnect() { showMessage("QML Debugger: Trying to connect ...", LogStatus); d->retryOnConnectFail = true; if (state() == EngineRunRequested) { if (isDying()) { // Probably cpp is being debugged and hence we did not get the output yet. appStartupFailed(tr("No application output received in time")); } else { beginConnection(); } } else { d->automaticConnect = true; } } void QmlEngine::beginConnection() { if (state() != EngineRunRequested && d->retryOnConnectFail) return; QTC_ASSERT(state() == EngineRunRequested, return); QObject::disconnect(d->startupMessageFilterConnection); QString host = runParameters().qmlServer.host(); // Use localhost as default if (host.isEmpty()) host = QHostAddress(QHostAddress::LocalHost).toString(); // FIXME: Not needed? /* * Let plugin-specific code override the port printed by the application. This is necessary * in the case of port forwarding, when the port the application listens on is not the same that * we want to connect to. * NOTE: It is still necessary to wait for the output in that case, because otherwise we cannot * be sure that the port is already open. The usual method of trying to connect repeatedly * will not work, because the intermediate port is already open. So the connection * will be accepted on that port but the forwarding to the target port will fail and * the connection will be closed again (instead of returning the "connection refused" * error that we expect). */ int port = runParameters().qmlServer.port(); QmlDebugConnection *connection = d->connection(); if (!connection || connection->isConnected()) return; connection->connectToHost(host, port); //A timeout to check the connection state d->connectionTimer.start(); } void QmlEngine::connectionStartupFailed() { if (d->retryOnConnectFail) { // retry after 3 seconds ... QTimer::singleShot(3000, this, [this] { beginConnection(); }); return; } auto infoBox = new QMessageBox(ICore::mainWindow()); infoBox->setIcon(QMessageBox::Critical); infoBox->setWindowTitle(Core::Constants::IDE_DISPLAY_NAME); infoBox->setText(tr("Could not connect to the in-process QML debugger." "\nDo you want to retry?")); infoBox->setStandardButtons(QMessageBox::Retry | QMessageBox::Cancel | QMessageBox::Help); infoBox->setDefaultButton(QMessageBox::Retry); infoBox->setModal(true); connect(infoBox, &QDialog::finished, this, &QmlEngine::errorMessageBoxFinished); infoBox->show(); } void QmlEngine::appStartupFailed(const QString &errorMessage) { QString error = tr("Could not connect to the in-process QML debugger. %1").arg(errorMessage); if (companionEngine()) { auto infoBox = new QMessageBox(ICore::mainWindow()); infoBox->setIcon(QMessageBox::Critical); infoBox->setWindowTitle(Core::Constants::IDE_DISPLAY_NAME); infoBox->setText(error); infoBox->setStandardButtons(QMessageBox::Ok | QMessageBox::Help); infoBox->setDefaultButton(QMessageBox::Ok); connect(infoBox, &QDialog::finished, this, &QmlEngine::errorMessageBoxFinished); infoBox->show(); } else { debuggerConsole()->printItem(ConsoleItem::WarningType, error); } notifyEngineRunFailed(); } void QmlEngine::errorMessageBoxFinished(int result) { switch (result) { case QMessageBox::Retry: { beginConnection(); break; } case QMessageBox::Help: { HelpManager::showHelpUrl("qthelp://org.qt-project.qtcreator/doc/creator-debugging-qml.html"); Q_FALLTHROUGH(); } default: if (state() == InferiorRunOk) { notifyInferiorSpontaneousStop(); notifyInferiorIll(); } else if (state() == EngineRunRequested) { notifyEngineRunFailed(); } break; } } void QmlEngine::gotoLocation(const Location &location) { const QString fileName = location.fileName(); if (QUrl(fileName).isLocalFile()) { // internal file from source files -> show generated .js QTC_ASSERT(d->sourceDocuments.contains(fileName), return); QString titlePattern = tr("JS Source for %1").arg(fileName); //Check if there are open documents with the same title foreach (IDocument *document, DocumentModel::openedDocuments()) { if (document->displayName() == titlePattern) { EditorManager::activateEditorForDocument(document); return; } } IEditor *editor = EditorManager::openEditorWithContents( QmlJSEditor::Constants::C_QMLJSEDITOR_ID, &titlePattern); if (editor) { editor->document()->setProperty(Constants::OPENED_BY_DEBUGGER, true); if (auto plainTextEdit = qobject_cast(editor->widget())) plainTextEdit->setReadOnly(true); updateDocument(editor->document(), d->sourceDocuments.value(fileName)); } } else { DebuggerEngine::gotoLocation(location); } } void QmlEngine::closeConnection() { if (d->connectionTimer.isActive()) { d->connectionTimer.stop(); } else { if (QmlDebugConnection *connection = d->connection()) connection->close(); } } void QmlEngine::runEngine() { // we won't get any debug output if (!terminal()) { d->retryOnConnectFail = true; d->automaticConnect = true; } QTC_ASSERT(state() == EngineRunRequested, qDebug() << state()); if (isPrimaryEngine()) { // QML only. if (runParameters().startMode == AttachToRemoteServer) tryToConnect(); else if (runParameters().startMode == AttachToRemoteProcess) beginConnection(); else startApplicationLauncher(); } else { tryToConnect(); } } void QmlEngine::startApplicationLauncher() { if (!d->applicationLauncher.isRunning()) { const Runnable runnable = runParameters().inferior; showMessage(tr("Starting %1 %2").arg(QDir::toNativeSeparators(runnable.executable), runnable.commandLineArguments), Utils::NormalMessageFormat); d->applicationLauncher.start(runnable); } } void QmlEngine::stopApplicationLauncher() { if (d->applicationLauncher.isRunning()) { disconnect(&d->applicationLauncher, &ApplicationLauncher::processExited, this, &QmlEngine::disconnected); d->applicationLauncher.stop(); } } void QmlEngine::shutdownInferior() { CHECK_STATE(InferiorShutdownRequested); // End session. // { "seq" : , // "type" : "request", // "command" : "disconnect", // } d->runCommand({DISCONNECT}); resetLocation(); stopApplicationLauncher(); closeConnection(); notifyInferiorShutdownFinished(); } void QmlEngine::shutdownEngine() { clearExceptionSelection(); debuggerConsole()->setScriptEvaluator(ScriptEvaluator()); // double check (ill engine?): stopApplicationLauncher(); notifyEngineShutdownFinished(); showMessage(QString(), StatusBar); } void QmlEngine::setupEngine() { notifyEngineSetupOk(); if (d->automaticConnect) beginConnection(); } void QmlEngine::continueInferior() { QTC_ASSERT(state() == InferiorStopOk, qDebug() << state()); clearExceptionSelection(); d->continueDebugging(Continue); resetLocation(); notifyInferiorRunRequested(); notifyInferiorRunOk(); } void QmlEngine::interruptInferior() { showMessage(INTERRUPT, LogInput); d->runDirectCommand(INTERRUPT); showStatusMessage(tr("Waiting for JavaScript engine to interrupt on next statement.")); } void QmlEngine::executeStepIn(bool) { clearExceptionSelection(); d->continueDebugging(StepIn); notifyInferiorRunRequested(); notifyInferiorRunOk(); } void QmlEngine::executeStepOut() { clearExceptionSelection(); d->continueDebugging(StepOut); notifyInferiorRunRequested(); notifyInferiorRunOk(); } void QmlEngine::executeStepOver(bool) { clearExceptionSelection(); d->continueDebugging(Next); notifyInferiorRunRequested(); notifyInferiorRunOk(); } void QmlEngine::executeRunToLine(const ContextData &data) { QTC_ASSERT(state() == InferiorStopOk, qDebug() << state()); showStatusMessage(tr("Run to line %1 (%2) requested...").arg(data.lineNumber).arg(data.fileName), 5000); d->setBreakpoint(SCRIPTREGEXP, data.fileName, true, data.lineNumber); clearExceptionSelection(); d->continueDebugging(Continue); notifyInferiorRunRequested(); notifyInferiorRunOk(); } void QmlEngine::executeRunToFunction(const QString &functionName) { Q_UNUSED(functionName) XSDEBUG("FIXME: QmlEngine::executeRunToFunction()"); } void QmlEngine::executeJumpToLine(const ContextData &data) { Q_UNUSED(data) XSDEBUG("FIXME: QmlEngine::executeJumpToLine()"); } void QmlEngine::activateFrame(int index) { if (state() != InferiorStopOk && state() != InferiorUnrunnable) return; stackHandler()->setCurrentIndex(index); gotoLocation(stackHandler()->frames().value(index)); d->updateLocals(); } void QmlEngine::selectThread(const Thread &thread) { Q_UNUSED(thread) } void QmlEngine::insertBreakpoint(const Breakpoint &bp) { QTC_ASSERT(bp, return); const BreakpointState state = bp->state(); QTC_ASSERT(state == BreakpointInsertionRequested, qDebug() << bp << this << state); notifyBreakpointInsertProceeding(bp); const BreakpointParameters &requested = bp->requestedParameters(); if (requested.type == BreakpointAtJavaScriptThrow) { bp->setPending(false); notifyBreakpointInsertOk(bp); d->setExceptionBreak(AllExceptions, requested.enabled); } else if (requested.type == BreakpointByFileAndLine) { d->setBreakpoint(SCRIPTREGEXP, requested.fileName, requested.enabled, requested.lineNumber, 0, requested.condition, requested.ignoreCount); } else if (requested.type == BreakpointOnQmlSignalEmit) { d->setBreakpoint(EVENT, requested.functionName, requested.enabled); bp->setPending(false); notifyBreakpointInsertOk(bp); } d->breakpointsSync.insert(d->sequence, bp); } void QmlEngine::resetLocation() { DebuggerEngine::resetLocation(); d->currentlyLookingUp.clear(); } void QmlEngine::removeBreakpoint(const Breakpoint &bp) { QTC_ASSERT(bp, return); const BreakpointParameters ¶ms = bp->requestedParameters(); const BreakpointState state = bp->state(); QTC_ASSERT(state == BreakpointRemoveRequested, qDebug() << bp << this << state); notifyBreakpointRemoveProceeding(bp); if (params.type == BreakpointAtJavaScriptThrow) d->setExceptionBreak(AllExceptions); else if (params.type == BreakpointOnQmlSignalEmit) d->setBreakpoint(EVENT, params.functionName, false); else d->clearBreakpoint(bp); if (bp->state() == BreakpointRemoveProceeding) notifyBreakpointRemoveOk(bp); } void QmlEngine::updateBreakpoint(const Breakpoint &bp) { QTC_ASSERT(bp, return); const BreakpointState state = bp->state(); QTC_ASSERT(state == BreakpointUpdateRequested, qDebug() << bp << this << state); notifyBreakpointChangeProceeding(bp); const BreakpointParameters &requested = bp->requestedParameters(); if (requested.type == BreakpointAtJavaScriptThrow) { d->setExceptionBreak(AllExceptions, requested.enabled); bp->setEnabled(requested.enabled); } else if (requested.type == BreakpointOnQmlSignalEmit) { d->setBreakpoint(EVENT, requested.functionName, requested.enabled); bp->setEnabled(requested.enabled); } else if (d->canChangeBreakpoint()) { d->changeBreakpoint(bp, requested.enabled); } else { d->clearBreakpoint(bp); d->setBreakpoint(SCRIPTREGEXP, requested.fileName, requested.enabled, requested.lineNumber, 0, requested.condition, requested.ignoreCount); d->breakpointsSync.insert(d->sequence, bp); } if (bp->state() == BreakpointUpdateProceeding) notifyBreakpointChangeOk(bp); } bool QmlEngine::acceptsBreakpoint(const BreakpointParameters &bp) const { //TODO: enable setting of breakpoints before start of debug session //For now, the event breakpoint can be set after the activeDebuggerClient is known //This is because the older client does not support BreakpointOnQmlSignalHandler if (bp.type == BreakpointOnQmlSignalEmit || bp.type == BreakpointAtJavaScriptThrow) return true; return bp.isQmlFileAndLineBreakpoint(); } void QmlEngine::loadSymbols(const QString &moduleName) { Q_UNUSED(moduleName) } void QmlEngine::loadAllSymbols() { } void QmlEngine::reloadModules() { } void QmlEngine::reloadSourceFiles() { d->scripts(4, QList(), true, QVariant()); } void QmlEngine::updateAll() { d->updateLocals(); } void QmlEngine::requestModuleSymbols(const QString &moduleName) { Q_UNUSED(moduleName) } bool QmlEngine::canHandleToolTip(const DebuggerToolTipContext &) const { // This is processed by QML inspector, which has dependencies to // the qml js editor. Makes life easier. // FIXME: Except that there isn't any attached. return true; } void QmlEngine::assignValueInDebugger(WatchItem *item, const QString &expression, const QVariant &editValue) { if (!expression.isEmpty()) { QVariant value = (editValue.type() == QVariant::String) ? QVariant('"' + editValue.toString().replace('"', "\\\"") + '"') : editValue; if (item->isInspect()) { d->inspectorAgent.assignValue(item, expression, value); } else { StackHandler *handler = stackHandler(); QString exp = QString("%1 = %2;").arg(expression).arg(value.toString()); if (handler->isContentsValid() && handler->currentFrame().isUsable()) { d->evaluate(exp, -1, [this](const QVariantMap &) { d->updateLocals(); }); } else { showMessage(tr("Cannot evaluate %1 in current stack frame.") .arg(expression), ConsoleOutput); } } } } void QmlEngine::expandItem(const QString &iname) { const WatchItem *item = watchHandler()->findItem(iname); QTC_ASSERT(item, return); if (item->isInspect()) { d->inspectorAgent.updateWatchData(*item); } else { LookupItems items; items.insert(int(item->id), {item->iname, item->name, item->exp}); d->lookup(items); } } void QmlEngine::updateItem(const QString &iname) { const WatchItem *item = watchHandler()->findItem(iname); QTC_ASSERT(item, return); if (state() == InferiorStopOk) { // The Qt side Q_ASSERTs otherwise. So postpone the evaluation, // it will be triggered from from upateLocals() later. QString exp = item->exp; d->evaluate(exp, -1, [this, iname, exp](const QVariantMap &response) { d->handleEvaluateExpression(response, iname, exp); }); } } void QmlEngine::selectWatchData(const QString &iname) { const WatchItem *item = watchHandler()->findItem(iname); if (item && item->isInspect()) d->inspectorAgent.watchDataSelected(item->id); } bool compareConsoleItems(const ConsoleItem *a, const ConsoleItem *b) { if (a == nullptr) return true; if (b == nullptr) return false; return a->text() < b->text(); } static ConsoleItem *constructLogItemTree(const QVariant &result, const QString &key = QString()) { bool sorted = boolSetting(SortStructMembers); if (!result.isValid()) return nullptr; QString text; ConsoleItem *item = nullptr; if (result.type() == QVariant::Map) { if (key.isEmpty()) text = "Object"; else text = key + " : Object"; QMap resultMap = result.toMap(); QVarLengthArray children(resultMap.size()); QMapIterator i(result.toMap()); auto it = children.begin(); while (i.hasNext()) { i.next(); *(it++) = constructLogItemTree(i.value(), i.key()); } // Sort before inserting as ConsoleItem::sortChildren causes a whole cascade of changes we // may not want to handle here. if (sorted) std::sort(children.begin(), children.end(), compareConsoleItems); item = new ConsoleItem(ConsoleItem::DefaultType, text); foreach (ConsoleItem *child, children) { if (child) item->appendChild(child); } } else if (result.type() == QVariant::List) { if (key.isEmpty()) text = "List"; else text = QString("[%1] : List").arg(key); QVariantList resultList = result.toList(); QVarLengthArray children(resultList.size()); for (int i = 0; i < resultList.count(); i++) children[i] = constructLogItemTree(resultList.at(i), QString::number(i)); if (sorted) std::sort(children.begin(), children.end(), compareConsoleItems); item = new ConsoleItem(ConsoleItem::DefaultType, text); foreach (ConsoleItem *child, children) { if (child) item->appendChild(child); } } else if (result.canConvert(QVariant::String)) { item = new ConsoleItem(ConsoleItem::DefaultType, result.toString()); } else { item = new ConsoleItem(ConsoleItem::DefaultType, "Unknown Value"); } return item; } void QmlEngine::expressionEvaluated(quint32 queryId, const QVariant &result) { if (d->queryIds.contains(queryId)) { d->queryIds.removeOne(queryId); if (ConsoleItem *item = constructLogItemTree(result)) debuggerConsole()->printItem(item); } } bool QmlEngine::hasCapability(unsigned cap) const { return cap & (AddWatcherCapability | AddWatcherWhileRunningCapability | RunToLineCapability | WatchComplexExpressionsCapability); /*ReverseSteppingCapability | SnapshotCapability | AutoDerefPointersCapability | DisassemblerCapability | RegisterCapability | ShowMemoryCapability | JumpToLineCapability | ReloadModuleCapability | ReloadModuleSymbolsCapability | BreakOnThrowAndCatchCapability | ReturnFromFunctionCapability | CreateFullBacktraceCapability | WatchpointCapability | AddWatcherCapability;*/ } void QmlEngine::quitDebugger() { d->automaticConnect = false; d->retryOnConnectFail = false; stopApplicationLauncher(); closeConnection(); } void QmlEngine::doUpdateLocals(const UpdateParameters ¶ms) { Q_UNUSED(params); d->updateLocals(); } Context QmlEngine::languageContext() const { return Context(Constants::C_QMLDEBUGGER); } void QmlEngine::disconnected() { showMessage(tr("QML Debugger disconnected."), StatusBar); notifyInferiorExited(); } void QmlEngine::updateCurrentContext() { d->inspectorAgent.enableTools(state() == InferiorRunOk); QString context; switch (state()) { case InferiorStopOk: context = stackHandler()->currentFrame().function; break; case InferiorRunOk: if (d->contextEvaluate || !d->unpausedEvaluate) { // !unpausedEvaluate means we are using the old QQmlEngineDebugService which understands // context. contextEvaluate means the V4 debug service can handle context. QModelIndex currentIndex = inspectorView()->currentIndex(); const WatchItem *currentData = watchHandler()->watchItem(currentIndex); if (!currentData) return; const WatchItem *parentData = watchHandler()->watchItem(currentIndex.parent()); const WatchItem *grandParentData = watchHandler()->watchItem(currentIndex.parent().parent()); if (currentData->id != parentData->id) context = currentData->name; else if (parentData->id != grandParentData->id) context = parentData->name; else context = grandParentData->name; } break; default: // No context. Clear the label debuggerConsole()->setContext(QString()); return; } debuggerConsole()->setContext(tr("Context:") + ' ' + (context.isEmpty() ? tr("Global QML Context") : context)); } void QmlEngine::executeDebuggerCommand(const QString &command) { if (state() == InferiorStopOk) { StackHandler *handler = stackHandler(); if (handler->isContentsValid() && handler->currentFrame().isUsable()) { d->evaluate(command, -1, CB(d->handleExecuteDebuggerCommand)); } else { // Paused but no stack? Something is wrong d->engine->showMessage(QString("Cannot evaluate %1. The stack trace is broken.").arg(command), ConsoleOutput); } } else { QModelIndex currentIndex = inspectorView()->currentIndex(); qint64 contextId = watchHandler()->watchItem(currentIndex)->id; if (d->unpausedEvaluate) { d->evaluate(command, contextId, CB(d->handleExecuteDebuggerCommand)); } else { quint32 queryId = d->inspectorAgent.queryExpressionResult( contextId, command, d->inspectorAgent.engineId(watchHandler()->watchItem(currentIndex))); if (queryId) { d->queryIds.append(queryId); } else { d->engine->showMessage("The application has to be stopped in a breakpoint in order to" " evaluate expressions", ConsoleOutput); } } } } bool QmlEngine::companionPreventsActions() const { // We need a C++ Engine in a Running state to do anything sensible // as otherwise the debugger services in the debuggee are unresponsive. if (DebuggerEngine *companion = companionEngine()) return companion->state() != InferiorRunOk; return false; } void QmlEnginePrivate::updateScriptSource(const QString &fileName, int lineOffset, int columnOffset, const QString &source) { QTextDocument *document = nullptr; if (sourceDocuments.contains(fileName)) { document = sourceDocuments.value(fileName); } else { document = new QTextDocument(this); sourceDocuments.insert(fileName, document); } // We're getting an unordered set of snippets that can even interleave // Therefore we've to carefully update the existing document QTextCursor cursor(document); for (int i = 0; i < lineOffset; ++i) { if (!cursor.movePosition(QTextCursor::NextBlock)) cursor.insertBlock(); } QTC_CHECK(cursor.blockNumber() == lineOffset); for (int i = 0; i < columnOffset; ++i) { if (!cursor.movePosition(QTextCursor::NextCharacter)) cursor.insertText(" "); } QTC_CHECK(cursor.positionInBlock() == columnOffset); const QStringList lines = source.split('\n'); for (QString line : lines) { if (line.endsWith('\r')) line.remove(line.size() -1, 1); // line already there? QTextCursor existingCursor(cursor); existingCursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor); if (existingCursor.selectedText() != line) cursor.insertText(line); if (!cursor.movePosition(QTextCursor::NextBlock)) cursor.insertBlock(); } //update open editors QString titlePattern = QCoreApplication::translate("QmlEngine", "JS Source for %1").arg(fileName); //Check if there are open editors with the same title foreach (IDocument *doc, DocumentModel::openedDocuments()) { if (doc->displayName() == titlePattern) { updateDocument(doc, document); break; } } } bool QmlEnginePrivate::canEvaluateScript(const QString &script) { interpreter.clearText(); interpreter.appendText(script); return interpreter.canEvaluate(); } void QmlEngine::connectionFailed() { // this is only an error if we are already connected and something goes wrong. if (isConnected()) { showMessage(tr("QML Debugger: Connection failed."), StatusBar); notifyInferiorSpontaneousStop(); notifyInferiorIll(); } else { d->connectionTimer.stop(); connectionStartupFailed(); } } void QmlEngine::checkConnectionState() { if (!isConnected()) { closeConnection(); connectionStartupFailed(); } } bool QmlEngine::isConnected() const { if (QmlDebugConnection *connection = d->connection()) return connection->isConnected(); else return false; } void QmlEngine::showConnectionStateMessage(const QString &message) { showMessage("QML Debugger: " + message, LogStatus); } void QmlEngine::logServiceStateChange(const QString &service, float version, QmlDebugClient::State newState) { switch (newState) { case QmlDebugClient::Unavailable: { showConnectionStateMessage(QString("Status of \"%1\" Version: %2 changed to 'unavailable'."). arg(service).arg(version)); break; } case QmlDebugClient::Enabled: { showConnectionStateMessage(QString("Status of \"%1\" Version: %2 changed to 'enabled'."). arg(service).arg(version)); break; } case QmlDebugClient::NotConnected: { showConnectionStateMessage(QString("Status of \"%1\" Version: %2 changed to 'not connected'."). arg(service).arg(version)); break; } } } void QmlEngine::logServiceActivity(const QString &service, const QString &logMessage) { showMessage(service + ' ' + logMessage, LogDebug); } void QmlEnginePrivate::continueDebugging(StepAction action) { // { "seq" : , // "type" : "request", // "command" : "continue", // "arguments" : { "stepaction" : <"in", "next" or "out">, // "stepcount" : // } // } DebuggerCommand cmd(CONTINEDEBUGGING); if (action == StepIn) cmd.arg(STEPACTION, IN); else if (action == StepOut) cmd.arg(STEPACTION, OUT); else if (action == Next) cmd.arg(STEPACTION, NEXT); runCommand(cmd); previousStepAction = action; } void QmlEnginePrivate::evaluate(const QString expr, qint64 context, const QmlCallback &cb) { // { "seq" : , // "type" : "request", // "command" : "evaluate", // "arguments" : { "expression" : , // "frame" : , // "global" : , // "disable_break" : , // "context" : // } // } // The Qt side Q_ASSERTs otherwise. So ignore the request and hope // it will be repeated soon enough (which it will, e.g. in updateLocals) QTC_ASSERT(unpausedEvaluate || engine->state() == InferiorStopOk, return); DebuggerCommand cmd(EVALUATE); cmd.arg(EXPRESSION, expr); StackHandler *handler = engine->stackHandler(); if (handler->currentFrame().isUsable()) cmd.arg(FRAME, handler->currentIndex()); if (context >= 0) cmd.arg(CONTEXT, context); runCommand(cmd, cb); } void QmlEnginePrivate::handleEvaluateExpression(const QVariantMap &response, const QString &iname, const QString &exp) { // { "seq" : , // "type" : "response", // "request_seq" : , // "command" : "evaluate", // "body" : ... // "running" : // "success" : true // } QVariant bodyVal = response.value(BODY).toMap(); QmlV8ObjectData body = extractData(bodyVal); WatchHandler *watchHandler = engine->watchHandler(); auto item = new WatchItem; item->iname = iname; item->name = exp; item->exp = exp; item->id = body.handle; bool success = response.value("success").toBool(); if (success) { item->type = body.type; item->value = body.value.toString(); setWatchItemHasChildren(item, body.hasChildren()); } else { //Do not set type since it is unknown item->setError(body.value.toString()); } insertSubItems(item, body.properties); watchHandler->insertItem(item); watchHandler->updateLocalsWindow(); } void QmlEnginePrivate::lookup(const LookupItems &items) { // { "seq" : , // "type" : "request", // "command" : "lookup", // "arguments" : { "handles" : , // "includeSource" : , // } // } if (items.isEmpty()) return; QList handles; for (auto it = items.begin(); it != items.end(); ++it) { const int handle = it.key(); if (!currentlyLookingUp.contains(handle)) { currentlyLookingUp.insert(handle, it.value()); handles.append(handle); } } DebuggerCommand cmd(LOOKUP); cmd.arg(HANDLES, handles); runCommand(cmd, CB(handleLookup)); } void QmlEnginePrivate::backtrace() { // { "seq" : , // "type" : "request", // "command" : "backtrace", // "arguments" : { "fromFrame" : // "toFrame" : // "bottom" : // } // } DebuggerCommand cmd(BACKTRACE); runCommand(cmd, CB(handleBacktrace)); } void QmlEnginePrivate::updateLocals() { // { "seq" : , // "type" : "request", // "command" : "frame", // "arguments" : { "number" : } // } DebuggerCommand cmd(FRAME); cmd.arg(NUMBER, stackIndexLookup.value(engine->stackHandler()->currentIndex())); runCommand(cmd, CB(handleFrame)); } void QmlEnginePrivate::scope(int number, int frameNumber) { // { "seq" : , // "type" : "request", // "command" : "scope", // "arguments" : { "number" : // "frameNumber" : // } // } DebuggerCommand cmd(SCOPE); cmd.arg(NUMBER, number); if (frameNumber != -1) cmd.arg(FRAMENUMBER, frameNumber); runCommand(cmd, CB(handleScope)); } void QmlEnginePrivate::scripts(int types, const QList ids, bool includeSource, const QVariant filter) { // { "seq" : , // "type" : "request", // "command" : "scripts", // "arguments" : { "types" : // "ids" : // "includeSource" : // "filter" : // } // } DebuggerCommand cmd(SCRIPTS); cmd.arg(TYPES, types); if (ids.count()) cmd.arg(IDS, ids); if (includeSource) cmd.arg(INCLUDESOURCE, includeSource); if (filter.type() == QVariant::String) cmd.arg(FILTER, filter.toString()); else if (filter.type() == QVariant::Int) cmd.arg(FILTER, filter.toInt()); else QTC_CHECK(!filter.isValid()); runCommand(cmd); } void QmlEnginePrivate::setBreakpoint(const QString type, const QString target, bool enabled, int line, int column, const QString condition, int ignoreCount) { // { "seq" : , // "type" : "request", // "command" : "setbreakpoint", // "arguments" : { "type" : <"function" or "script" or "scriptId" or "scriptRegExp"> // "target" : // "line" : // "column" : // "enabled" : // "condition" : // "ignoreCount" : // } // } if (type == EVENT) { QPacket rs(dataStreamVersion()); rs << target.toUtf8() << enabled; engine->showMessage(QString("%1 %2 %3") .arg(BREAKONSIGNAL, target, QLatin1String(enabled ? "enabled" : "disabled")), LogInput); runDirectCommand(BREAKONSIGNAL, rs.data()); } else { DebuggerCommand cmd(SETBREAKPOINT); cmd.arg(TYPE, type); cmd.arg(ENABLED, enabled); if (type == SCRIPTREGEXP) cmd.arg(TARGET, Utils::FilePath::fromString(target).fileName()); else cmd.arg(TARGET, target); if (line) cmd.arg(LINE, line - 1); if (column) cmd.arg(COLUMN, column - 1); if (!condition.isEmpty()) cmd.arg(CONDITION, condition); if (ignoreCount != -1) cmd.arg(IGNORECOUNT, ignoreCount); runCommand(cmd); } } void QmlEnginePrivate::clearBreakpoint(const Breakpoint &bp) { // { "seq" : , // "type" : "request", // "command" : "clearbreakpoint", // "arguments" : { "breakpoint" : // } // } DebuggerCommand cmd(CLEARBREAKPOINT); cmd.arg(BREAKPOINT, bp->responseId().toInt()); runCommand(cmd); } bool QmlEnginePrivate::canChangeBreakpoint() const { return supportChangeBreakpoint; } void QmlEnginePrivate::changeBreakpoint(const Breakpoint &bp, bool enabled) { DebuggerCommand cmd(CHANGEBREAKPOINT); cmd.arg(BREAKPOINT, bp->responseId().toInt()); cmd.arg(ENABLED, enabled); runCommand(cmd); } void QmlEnginePrivate::setExceptionBreak(Exceptions type, bool enabled) { // { "seq" : , // "type" : "request", // "command" : "setexceptionbreak", // "arguments" : { "type" : , // "enabled" : // } // } DebuggerCommand cmd(SETEXCEPTIONBREAK); if (type == AllExceptions) cmd.arg(TYPE, ALL); //Not Supported: // else if (type == UncaughtExceptions) // cmd.args(TYPE, UNCAUGHT); if (enabled) cmd.arg(ENABLED, enabled); runCommand(cmd); } QmlV8ObjectData QmlEnginePrivate::extractData(const QVariant &data) const { // { "handle" : , // "type" : <"undefined", "null", "boolean", "number", "string", "object", "function" or "frame"> // } // {"handle":,"type":"undefined"} // {"handle":,"type":"null"} // { "handle":, // "type" : <"boolean", "number" or "string"> // "value" : // } // {"handle":7,"type":"boolean","value":true} // {"handle":8,"type":"number","value":42} // { "handle" : , // "type" : "object", // "className" : , // "constructorFunction" : {"ref":}, // "protoObject" : {"ref":}, // "prototypeObject" : {"ref":}, // "properties" : [ {"name" : , // "ref" : // }, // ... // ] // } // { "handle" : , // "type" : "function", // "className" : "Function", // "constructorFunction" : {"ref":}, // "protoObject" : {"ref":}, // "prototypeObject" : {"ref":}, // "name" : , // "inferredName" : // "source" : , // "script" : , // "scriptId" : , // "position" : , // "line" : , // "column" : , // "properties" : [ {"name" : , // "ref" : // }, // ... // ] // } QmlV8ObjectData objectData; const QVariantMap dataMap = data.toMap(); objectData.name = dataMap.value(NAME).toString(); QString type = dataMap.value(TYPE).toString(); objectData.handle = dataMap.value(HANDLE).toInt(); if (type == "undefined") { objectData.type = "undefined"; objectData.value = "undefined"; } else if (type == "null") { // Deprecated. typeof(null) == "object" in JavaScript objectData.type = "object"; objectData.value = "null"; } else if (type == "boolean") { objectData.type = "boolean"; objectData.value = dataMap.value(VALUE); } else if (type == "number") { objectData.type = "number"; objectData.value = dataMap.value(VALUE); } else if (type == "string") { QChar quote('"'); objectData.type = "string"; objectData.value = QString(quote + dataMap.value(VALUE).toString() + quote); } else if (type == "object") { objectData.type = "object"; // ignore "className": it doesn't make any sense. if (dataMap.contains("value")) { QVariant value = dataMap.value("value"); // The QVariant representation of null has changed across various Qt versions // 5.6, 5.7: QVariant::Invalid // 5.8: isValid(), !isNull(), type() == 51; only typeName() is unique: "std::nullptr_t" // 5.9: isValid(), isNull(); We can then use isNull() if (!value.isValid() || value.isNull() || strcmp(value.typeName(), "std::nullptr_t") == 0) { objectData.value = "null"; // Yes, null is an object. } else if (value.isValid()) { objectData.expectedProperties = value.toInt(); } } if (dataMap.contains("properties")) objectData.properties = dataMap.value("properties").toList(); } else if (type == "function") { objectData.type = "function"; objectData.value = dataMap.value(NAME); objectData.properties = dataMap.value("properties").toList(); QVariant value = dataMap.value("value"); if (value.isValid()) objectData.expectedProperties = value.toInt(); } else if (type == "script") { objectData.type = "script"; objectData.value = dataMap.value(NAME); } if (dataMap.contains(REF)) { objectData.handle = dataMap.value(REF).toInt(); if (refVals.contains(objectData.handle)) { QmlV8ObjectData data = refVals.value(objectData.handle); if (objectData.type.isEmpty()) objectData.type = data.type; if (!objectData.value.isValid()) objectData.value = data.value; if (objectData.properties.isEmpty()) objectData.properties = data.properties; if (objectData.expectedProperties < 0) objectData.expectedProperties = data.expectedProperties; } } return objectData; } void QmlEnginePrivate::runCommand(const DebuggerCommand &command, const QmlCallback &cb) { QJsonObject object; object.insert("seq", ++sequence); object.insert("type", "request"); object.insert("command", command.function); object.insert("arguments", command.args); if (cb) callbackForToken[sequence] = cb; runDirectCommand(V8REQUEST, QJsonDocument(object).toJson(QJsonDocument::Compact)); } void QmlEnginePrivate::runDirectCommand(const QString &type, const QByteArray &msg) { // Leave item as variable, serialization depends on it. QByteArray cmd = V8DEBUG; engine->showMessage(QString("%1 %2").arg(type, QString::fromLatin1(msg)), LogInput); QPacket rs(dataStreamVersion()); rs << cmd << type.toLatin1() << msg; if (state() == Enabled) sendMessage(rs.data()); else sendBuffer.append(rs.data()); } void QmlEnginePrivate::memorizeRefs(const QVariant &refs) { if (refs.isValid()) { foreach (const QVariant &ref, refs.toList()) { const QVariantMap refData = ref.toMap(); int handle = refData.value(HANDLE).toInt(); refVals[handle] = extractData(refData); } } } void QmlEnginePrivate::messageReceived(const QByteArray &data) { QPacket ds(dataStreamVersion(), data); QByteArray command; ds >> command; if (command == V8DEBUG) { QByteArray type; QByteArray response; ds >> type >> response; engine->showMessage(QLatin1String(type), LogOutput); if (type == CONNECT) { //debugging session started } else if (type == INTERRUPT) { //debug break requested } else if (type == BREAKONSIGNAL) { //break on signal handler requested } else if (type == V8MESSAGE) { SDEBUG(response); engine->showMessage(QString(V8MESSAGE) + ' ' + QString::fromLatin1(response), LogOutput); const QVariantMap resp = QJsonDocument::fromJson(response).toVariant().toMap(); const QString type = resp.value(TYPE).toString(); if (type == "response") { const QString debugCommand(resp.value(COMMAND).toString()); memorizeRefs(resp.value(REFS)); bool success = resp.value("success").toBool(); if (!success) { SDEBUG("Request was unsuccessful"); } int requestSeq = resp.value("request_seq").toInt(); if (callbackForToken.contains(requestSeq)) { callbackForToken[requestSeq](resp); } else if (debugCommand == DISCONNECT) { //debugging session ended } else if (debugCommand == CONTINEDEBUGGING) { //do nothing, wait for next break } else if (debugCommand == SETBREAKPOINT) { // { "seq" : , // "type" : "response", // "request_seq" : , // "command" : "setbreakpoint", // "body" : { "type" : <"function" or "script"> // "breakpoint" : // } // "running" : // "success" : true // } int seq = resp.value("request_seq").toInt(); const QVariantMap breakpointData = resp.value(BODY).toMap(); const QString index = QString::number(breakpointData.value("breakpoint").toInt()); if (breakpointsSync.contains(seq)) { Breakpoint bp = breakpointsSync.take(seq); QTC_ASSERT(bp, return); bp->setParameters(bp->requestedParameters()); // Assume it worked. bp->setResponseId(index); //Is actual position info present? Then breakpoint was //accepted const QVariantList actualLocations = breakpointData.value("actual_locations").toList(); const int line = breakpointData.value("line").toInt() + 1; if (actualLocations.count()) { //The breakpoint requested line should be same as //actual line if (bp && bp->state() != BreakpointInserted) { bp->setLineNumber(line); bp->setPending(false); engine->notifyBreakpointInsertOk(bp); } } } else { breakpointsTemp.append(index); } } else if (debugCommand == CLEARBREAKPOINT) { // DO NOTHING } else if (debugCommand == SETEXCEPTIONBREAK) { // { "seq" : , // "type" : "response", // "request_seq" : , // "command" : "setexceptionbreak", // "body" : { "type" : , // "enabled" : // } // "running" : true // "success" : true // } } else if (debugCommand == SCRIPTS) { // { "seq" : , // "type" : "response", // "request_seq" : , // "command" : "scripts", // "body" : [ { "name" : , // "id" : // "lineOffset" : // "columnOffset" : // "lineCount" : // "data" : // "source" : // "sourceStart" : // "sourceLength" : // "scriptType" :