/**************************************************************************** ** ** Copyright (C) 2016 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of the QtQml module of the Qt Toolkit. ** ** $QT_BEGIN_LICENSE:LGPL$ ** 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 Lesser General Public License Usage ** Alternatively, this file may be used under the terms of the GNU Lesser ** General Public License version 3 as published by the Free Software ** Foundation and appearing in the file LICENSE.LGPL3 included in the ** packaging of this file. Please review the following information to ** ensure the GNU Lesser General Public License version 3 requirements ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. ** ** GNU General Public License Usage ** Alternatively, this file may be used under the terms of the GNU ** General Public License version 2.0 or (at your option) the GNU General ** Public license version 3 or any later version approved by the KDE Free ** Qt Foundation. The licenses are as published by the Free Software ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 ** 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-2.0.html and ** https://www.gnu.org/licenses/gpl-3.0.html. ** ** $QT_END_LICENSE$ ** ****************************************************************************/ #include "qqmlnativedebugservice.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include //#define TRACE_PROTOCOL(s) qDebug() << s #define TRACE_PROTOCOL(s) QT_BEGIN_NAMESPACE using QQmlDebugPacket = QVersionedPacket; class BreakPoint { public: BreakPoint() : id(-1), lineNumber(-1), enabled(false), ignoreCount(0), hitCount(0) {} bool isValid() const { return lineNumber >= 0 && !fileName.isEmpty(); } int id; int lineNumber; QString fileName; bool enabled; QString condition; int ignoreCount; int hitCount; }; inline uint qHash(const BreakPoint &b, uint seed = 0) Q_DECL_NOTHROW { return qHash(b.fileName, seed) ^ b.lineNumber; } inline bool operator==(const BreakPoint &a, const BreakPoint &b) { return a.lineNumber == b.lineNumber && a.fileName == b.fileName && a.enabled == b.enabled && a.condition == b.condition && a.ignoreCount == b.ignoreCount; } static void setError(QJsonObject *response, const QString &msg) { response->insert(QStringLiteral("type"), QStringLiteral("error")); response->insert(QStringLiteral("msg"), msg); } class NativeDebugger; class Collector { public: Collector(QV4::ExecutionEngine *engine) : m_engine(engine), m_anonCount(0) {} void collect(QJsonArray *output, const QString &parentIName, const QString &name, const QV4::Value &value); bool isExpanded(const QString &iname) const { return m_expanded.contains(iname); } public: QV4::ExecutionEngine *m_engine; int m_anonCount; QStringList m_expanded; }; // Encapsulate Breakpoint handling // Could be made per-NativeDebugger (i.e. per execution engine, if needed) class BreakPointHandler { public: BreakPointHandler() : m_haveBreakPoints(false), m_breakOnThrow(true), m_lastBreakpoint(1) {} void handleSetBreakpoint(QJsonObject *response, const QJsonObject &arguments); void handleRemoveBreakpoint(QJsonObject *response, const QJsonObject &arguments); void removeBreakPoint(int id); void enableBreakPoint(int id, bool onoff); void setBreakOnThrow(bool onoff); bool m_haveBreakPoints; bool m_breakOnThrow; int m_lastBreakpoint; QVector m_breakPoints; }; void BreakPointHandler::handleSetBreakpoint(QJsonObject *response, const QJsonObject &arguments) { TRACE_PROTOCOL("SET BREAKPOINT" << arguments); QString type = arguments.value(QLatin1String("type")).toString(); QString fileName = arguments.value(QLatin1String("file")).toString(); if (fileName.isEmpty()) { setError(response, QStringLiteral("breakpoint has no file name")); return; } int line = arguments.value(QLatin1String("line")).toInt(-1); if (line < 0) { setError(response, QStringLiteral("breakpoint has an invalid line number")); return; } BreakPoint bp; bp.id = m_lastBreakpoint++; bp.fileName = fileName.mid(fileName.lastIndexOf('/') + 1); bp.lineNumber = line; bp.enabled = arguments.value(QLatin1String("enabled")).toBool(true); bp.condition = arguments.value(QLatin1String("condition")).toString(); bp.ignoreCount = arguments.value(QLatin1String("ignorecount")).toInt(); m_breakPoints.append(bp); m_haveBreakPoints = true; response->insert(QStringLiteral("type"), type); response->insert(QStringLiteral("breakpoint"), bp.id); } void BreakPointHandler::handleRemoveBreakpoint(QJsonObject *response, const QJsonObject &arguments) { int id = arguments.value(QLatin1String("id")).toInt(); removeBreakPoint(id); response->insert(QStringLiteral("id"), id); } class NativeDebugger : public QV4::Debugging::Debugger { public: NativeDebugger(QQmlNativeDebugServiceImpl *service, QV4::ExecutionEngine *engine); void signalEmitted(const QString &signal); QV4::ExecutionEngine *engine() const { return m_engine; } bool pauseAtNextOpportunity() const override { return m_pauseRequested || m_service->m_breakHandler->m_haveBreakPoints || m_stepping >= StepOver; } void maybeBreakAtInstruction() override; void enteringFunction() override; void leavingFunction(const QV4::ReturnedValue &retVal) override; void aboutToThrow() override; void handleCommand(QJsonObject *response, const QString &cmd, const QJsonObject &arguments); private: void handleBacktrace(QJsonObject *response, const QJsonObject &arguments); void handleVariables(QJsonObject *response, const QJsonObject &arguments); void handleExpressions(QJsonObject *response, const QJsonObject &arguments); void handleDebuggerDeleted(QObject *debugger); QV4::ReturnedValue evaluateExpression(const QString &expression); bool checkCondition(const QString &expression); QStringList breakOnSignals; enum Speed { NotStepping = 0, StepOut, StepOver, StepIn, }; void pauseAndWait(); void pause(); void handleContinue(QJsonObject *reponse, Speed speed); QV4::Function *getFunction() const; bool reallyHitTheBreakPoint(const QV4::Function *function, int lineNumber); QV4::ExecutionEngine *m_engine; QQmlNativeDebugServiceImpl *m_service; QV4::CppStackFrame *m_currentFrame = nullptr; Speed m_stepping; bool m_pauseRequested; bool m_runningJob; QV4::PersistentValue m_returnedValue; }; bool NativeDebugger::checkCondition(const QString &expression) { QV4::Scope scope(m_engine); QV4::ScopedValue r(scope, evaluateExpression(expression)); return r->booleanValue(); } QV4::ReturnedValue NativeDebugger::evaluateExpression(const QString &expression) { QV4::Scope scope(m_engine); m_runningJob = true; QV4::ExecutionContext *ctx = m_engine->currentStackFrame ? m_engine->currentContext() : m_engine->rootContext(); QV4::Script script(ctx, QV4::Compiler::EvalCode, expression); if (const QV4::Function *function = m_engine->currentStackFrame ? m_engine->currentStackFrame->v4Function : m_engine->globalCode) script.strictMode = function->isStrict(); // In order for property lookups in QML to work, we need to disable fast v4 lookups. // That is a side-effect of inheritContext. script.inheritContext = true; script.parse(); if (!m_engine->hasException) { if (m_engine->currentStackFrame) { QV4::ScopedValue thisObject(scope, m_engine->currentStackFrame->thisObject()); script.run(thisObject); } else { script.run(); } } m_runningJob = false; return QV4::Encode::undefined(); } NativeDebugger::NativeDebugger(QQmlNativeDebugServiceImpl *service, QV4::ExecutionEngine *engine) : m_returnedValue(engine, QV4::Primitive::undefinedValue()) { m_stepping = NotStepping; m_pauseRequested = false; m_runningJob = false; m_service = service; m_engine = engine; TRACE_PROTOCOL("Creating native debugger"); } void NativeDebugger::signalEmitted(const QString &signal) { //This function is only called by QQmlBoundSignal //only if there is a slot connected to the signal. Hence, there //is no need for additional check. //Parse just the name and remove the class info //Normalize to Lower case. QString signalName = signal.left(signal.indexOf(QLatin1Char('('))).toLower(); for (const QString &signal : qAsConst(breakOnSignals)) { if (signal == signalName) { // TODO: pause debugger break; } } } void NativeDebugger::handleCommand(QJsonObject *response, const QString &cmd, const QJsonObject &arguments) { if (cmd == QLatin1String("backtrace")) handleBacktrace(response, arguments); else if (cmd == QLatin1String("variables")) handleVariables(response, arguments); else if (cmd == QLatin1String("expressions")) handleExpressions(response, arguments); else if (cmd == QLatin1String("stepin")) handleContinue(response, StepIn); else if (cmd == QLatin1String("stepout")) handleContinue(response, StepOut); else if (cmd == QLatin1String("stepover")) handleContinue(response, StepOver); else if (cmd == QLatin1String("continue")) handleContinue(response, NotStepping); } static QString encodeFrame(QV4::CppStackFrame *f) { QQmlDebugPacket ds; ds << quintptr(f); return QString::fromLatin1(ds.data().toHex()); } static void decodeFrame(const QString &f, QV4::CppStackFrame **frame) { quintptr rawFrame; QQmlDebugPacket ds(QByteArray::fromHex(f.toLatin1())); ds >> rawFrame; *frame = reinterpret_cast(rawFrame); } void NativeDebugger::handleBacktrace(QJsonObject *response, const QJsonObject &arguments) { int limit = arguments.value(QLatin1String("limit")).toInt(0); QJsonArray frameArray; QV4::CppStackFrame *f= m_engine->currentStackFrame; for (int i = 0; i < limit && f; ++i) { QV4::Function *function = f->v4Function; QJsonObject frame; frame.insert(QStringLiteral("language"), QStringLiteral("js")); frame.insert(QStringLiteral("context"), encodeFrame(f)); if (QV4::Heap::String *functionName = function->name()) frame.insert(QStringLiteral("function"), functionName->toQString()); frame.insert(QStringLiteral("file"), function->sourceFile()); int line = f->lineNumber(); frame.insert(QStringLiteral("line"), (line < 0 ? -line : line)); frameArray.push_back(frame); f = f->parent; } response->insert(QStringLiteral("frames"), frameArray); } void Collector::collect(QJsonArray *out, const QString &parentIName, const QString &name, const QV4::Value &value) { QJsonObject dict; QV4::Scope scope(m_engine); QString nonEmptyName = name.isEmpty() ? QString::fromLatin1("@%1").arg(m_anonCount++) : name; QString iname = parentIName + QLatin1Char('.') + nonEmptyName; dict.insert(QStringLiteral("iname"), iname); dict.insert(QStringLiteral("name"), nonEmptyName); QV4::ScopedValue typeString(scope, QV4::Runtime::method_typeofValue(m_engine, value)); dict.insert(QStringLiteral("type"), typeString->toQStringNoThrow()); switch (value.type()) { case QV4::Value::Empty_Type: dict.insert(QStringLiteral("valueencoded"), QStringLiteral("empty")); dict.insert(QStringLiteral("haschild"), false); break; case QV4::Value::Undefined_Type: dict.insert(QStringLiteral("valueencoded"), QStringLiteral("undefined")); dict.insert(QStringLiteral("haschild"), false); break; case QV4::Value::Null_Type: dict.insert(QStringLiteral("type"), QStringLiteral("object")); dict.insert(QStringLiteral("valueencoded"), QStringLiteral("null")); dict.insert(QStringLiteral("haschild"), false); break; case QV4::Value::Boolean_Type: dict.insert(QStringLiteral("value"), value.booleanValue()); dict.insert(QStringLiteral("haschild"), false); break; case QV4::Value::Managed_Type: if (const QV4::String *string = value.as()) { dict.insert(QStringLiteral("value"), string->toQStringNoThrow()); dict.insert(QStringLiteral("haschild"), false); dict.insert(QStringLiteral("valueencoded"), QStringLiteral("utf16")); dict.insert(QStringLiteral("quoted"), true); } else if (const QV4::ArrayObject *array = value.as()) { // The size of an array is number of its numerical properties. // We don't consider free form object properties here. const uint n = array->getLength(); dict.insert(QStringLiteral("value"), qint64(n)); dict.insert(QStringLiteral("valueencoded"), QStringLiteral("itemcount")); dict.insert(QStringLiteral("haschild"), qint64(n)); if (isExpanded(iname)) { QJsonArray children; for (uint i = 0; i < n; ++i) { QV4::ReturnedValue v = array->getIndexed(i); QV4::ScopedValue sval(scope, v); collect(&children, iname, QString::number(i), *sval); } dict.insert(QStringLiteral("children"), children); } } else if (const QV4::Object *object = value.as()) { QJsonArray children; bool expanded = isExpanded(iname); qint64 numProperties = 0; QV4::ObjectIterator it(scope, object, QV4::ObjectIterator::EnumerableOnly); QV4::ScopedProperty p(scope); QV4::ScopedString name(scope); while (true) { QV4::PropertyAttributes attrs; uint index; it.next(name.getRef(), &index, p, &attrs); if (attrs.isEmpty()) break; if (name.getPointer()) { ++numProperties; if (expanded) { if (name.getPointer()) { QV4::Value v = p.property->value; collect(&children, iname, name->toQStringNoThrow(), v); } } } } dict.insert(QStringLiteral("value"), numProperties); dict.insert(QStringLiteral("valueencoded"), QStringLiteral("itemcount")); dict.insert(QStringLiteral("haschild"), numProperties > 0); if (expanded) dict.insert(QStringLiteral("children"), children); } break; case QV4::Value::Integer_Type: dict.insert(QStringLiteral("value"), value.integerValue()); dict.insert(QStringLiteral("haschild"), false); break; default: // double dict.insert(QStringLiteral("value"), value.doubleValue()); dict.insert(QStringLiteral("haschild"), false); } out->append(dict); } void NativeDebugger::handleVariables(QJsonObject *response, const QJsonObject &arguments) { TRACE_PROTOCOL("Build variables"); QV4::CppStackFrame *frame = nullptr; decodeFrame(arguments.value(QLatin1String("context")).toString(), &frame); if (!frame) { setError(response, QStringLiteral("No stack frame passed")); return; } TRACE_PROTOCOL("Context: " << frame); QV4::ExecutionEngine *engine = frame->v4Function->internalClass->engine; if (!engine) { setError(response, QStringLiteral("No execution engine passed")); return; } TRACE_PROTOCOL("Engine: " << engine); Collector collector(engine); const QJsonArray expanded = arguments.value(QLatin1String("expanded")).toArray(); for (const QJsonValue &ex : expanded) collector.m_expanded.append(ex.toString()); TRACE_PROTOCOL("Expanded: " << collector.m_expanded); QJsonArray output; QV4::Scope scope(engine); QV4::ScopedValue thisObject(scope, frame->thisObject()); collector.collect(&output, QString(), QStringLiteral("this"), thisObject); QV4::Scoped callContext(scope, frame->callContext()); if (callContext) { QV4::InternalClass *ic = callContext->internalClass(); QV4::ScopedValue v(scope); for (uint i = 0; i < ic->size; ++i) { QString name = ic->nameMap[i]->string; v = callContext->d()->locals[i]; collector.collect(&output, QString(), name, v); } } response->insert(QStringLiteral("variables"), output); } void NativeDebugger::handleExpressions(QJsonObject *response, const QJsonObject &arguments) { TRACE_PROTOCOL("Evaluate expressions"); QV4::CppStackFrame *frame = nullptr; decodeFrame(arguments.value(QLatin1String("context")).toString(), &frame); if (!frame) { setError(response, QStringLiteral("No stack frame passed")); return; } TRACE_PROTOCOL("Context: " << executionContext); QV4::ExecutionEngine *engine = frame->v4Function->internalClass->engine; if (!engine) { setError(response, QStringLiteral("No execution engine passed")); return; } TRACE_PROTOCOL("Engines: " << engine << m_engine); Collector collector(engine); const QJsonArray expanded = arguments.value(QLatin1String("expanded")).toArray(); for (const QJsonValue &ex : expanded) collector.m_expanded.append(ex.toString()); TRACE_PROTOCOL("Expanded: " << collector.m_expanded); QJsonArray output; QV4::Scope scope(engine); const QJsonArray expressions = arguments.value(QLatin1String("expressions")).toArray(); for (const QJsonValue &expr : expressions) { QString expression = expr.toObject().value(QLatin1String("expression")).toString(); QString name = expr.toObject().value(QLatin1String("name")).toString(); TRACE_PROTOCOL("Evaluate expression: " << expression); m_runningJob = true; QV4::ScopedValue result(scope, evaluateExpression(expression)); m_runningJob = false; if (result->isUndefined()) { QJsonObject dict; dict.insert(QStringLiteral("name"), name); dict.insert(QStringLiteral("valueencoded"), QStringLiteral("undefined")); output.append(dict); } else if (result.ptr && result.ptr->rawValue()) { collector.collect(&output, QString(), name, *result); } else { QJsonObject dict; dict.insert(QStringLiteral("name"), name); dict.insert(QStringLiteral("valueencoded"), QStringLiteral("notaccessible")); output.append(dict); } TRACE_PROTOCOL("EXCEPTION: " << engine->hasException); engine->hasException = false; } response->insert(QStringLiteral("expressions"), output); } void BreakPointHandler::removeBreakPoint(int id) { for (int i = 0; i != m_breakPoints.size(); ++i) { if (m_breakPoints.at(i).id == id) { m_breakPoints.remove(i); m_haveBreakPoints = !m_breakPoints.isEmpty(); return; } } } void BreakPointHandler::enableBreakPoint(int id, bool enabled) { m_breakPoints[id].enabled = enabled; } void NativeDebugger::pause() { m_pauseRequested = true; } void NativeDebugger::handleContinue(QJsonObject *response, Speed speed) { Q_UNUSED(response); if (!m_returnedValue.isUndefined()) m_returnedValue.set(m_engine, QV4::Encode::undefined()); m_currentFrame = m_engine->currentStackFrame; m_stepping = speed; } void NativeDebugger::maybeBreakAtInstruction() { if (m_runningJob) // do not re-enter when we're doing a job for the debugger. return; if (m_stepping == StepOver) { if (m_currentFrame == m_engine->currentStackFrame) pauseAndWait(); return; } if (m_stepping == StepIn) { pauseAndWait(); return; } if (m_pauseRequested) { // Serve debugging requests from the agent m_pauseRequested = false; pauseAndWait(); return; } if (m_service->m_breakHandler->m_haveBreakPoints) { if (QV4::Function *function = getFunction()) { // lineNumber will be negative for Ret instructions, so those won't match const int lineNumber = m_engine->currentStackFrame->lineNumber(); if (reallyHitTheBreakPoint(function, lineNumber)) pauseAndWait(); } } } void NativeDebugger::enteringFunction() { if (m_runningJob) return; if (m_stepping == StepIn) { m_currentFrame = m_engine->currentStackFrame; } } void NativeDebugger::leavingFunction(const QV4::ReturnedValue &retVal) { if (m_runningJob) return; if (m_stepping != NotStepping && m_currentFrame == m_engine->currentStackFrame) { m_currentFrame = m_currentFrame->parent; m_stepping = StepOver; m_returnedValue.set(m_engine, retVal); } } void NativeDebugger::aboutToThrow() { if (!m_service->m_breakHandler->m_breakOnThrow) return; if (m_runningJob) // do not re-enter when we're doing a job for the debugger. return; QJsonObject event; // TODO: complete this! event.insert(QStringLiteral("event"), QStringLiteral("exception")); m_service->emitAsynchronousMessageToClient(event); } QV4::Function *NativeDebugger::getFunction() const { if (m_engine->currentStackFrame) return m_engine->currentStackFrame->v4Function; else return m_engine->globalCode; } void NativeDebugger::pauseAndWait() { QJsonObject event; event.insert(QStringLiteral("event"), QStringLiteral("break")); event.insert(QStringLiteral("language"), QStringLiteral("js")); if (QV4::CppStackFrame *frame = m_engine->currentStackFrame) { QV4::Function *function = frame->v4Function; event.insert(QStringLiteral("file"), function->sourceFile()); int line = frame->lineNumber(); event.insert(QStringLiteral("line"), (line < 0 ? -line : line)); } m_service->emitAsynchronousMessageToClient(event); } bool NativeDebugger::reallyHitTheBreakPoint(const QV4::Function *function, int lineNumber) { for (int i = 0, n = m_service->m_breakHandler->m_breakPoints.size(); i != n; ++i) { const BreakPoint &bp = m_service->m_breakHandler->m_breakPoints.at(i); if (bp.lineNumber == lineNumber) { const QString fileName = function->sourceFile(); const QStringRef base = fileName.midRef(fileName.lastIndexOf('/') + 1); if (bp.fileName.endsWith(base)) { if (bp.condition.isEmpty() || checkCondition(bp.condition)) { BreakPoint &mbp = m_service->m_breakHandler->m_breakPoints[i]; ++mbp.hitCount; if (mbp.hitCount > mbp.ignoreCount) return true; } } } } return false; } QQmlNativeDebugServiceImpl::QQmlNativeDebugServiceImpl(QObject *parent) : QQmlNativeDebugService(1.0, parent) { m_breakHandler = new BreakPointHandler; } QQmlNativeDebugServiceImpl::~QQmlNativeDebugServiceImpl() { delete m_breakHandler; } void QQmlNativeDebugServiceImpl::engineAboutToBeAdded(QJSEngine *engine) { TRACE_PROTOCOL("Adding engine" << engine); if (engine) { QV4::ExecutionEngine *ee = engine->handle(); TRACE_PROTOCOL("Adding execution engine" << ee); if (ee) { NativeDebugger *debugger = new NativeDebugger(this, ee); if (state() == Enabled) ee->setDebugger(debugger); m_debuggers.append(QPointer(debugger)); } } QQmlDebugService::engineAboutToBeAdded(engine); } void QQmlNativeDebugServiceImpl::engineAboutToBeRemoved(QJSEngine *engine) { TRACE_PROTOCOL("Removing engine" << engine); if (engine) { QV4::ExecutionEngine *executionEngine = engine->handle(); const auto debuggersCopy = m_debuggers; for (NativeDebugger *debugger : debuggersCopy) { if (debugger->engine() == executionEngine) m_debuggers.removeAll(debugger); } } QQmlDebugService::engineAboutToBeRemoved(engine); } void QQmlNativeDebugServiceImpl::stateAboutToBeChanged(QQmlDebugService::State state) { if (state == Enabled) { for (NativeDebugger *debugger : qAsConst(m_debuggers)) { QV4::ExecutionEngine *engine = debugger->engine(); if (!engine->debugger()) engine->setDebugger(debugger); } } QQmlDebugService::stateAboutToBeChanged(state); } void QQmlNativeDebugServiceImpl::messageReceived(const QByteArray &message) { TRACE_PROTOCOL("Native message received: " << message); QJsonObject request = QJsonDocument::fromJson(message).object(); QJsonObject response; QJsonObject arguments = request.value(QLatin1String("arguments")).toObject(); QString cmd = request.value(QLatin1String("command")).toString(); if (cmd == QLatin1String("setbreakpoint")) { m_breakHandler->handleSetBreakpoint(&response, arguments); } else if (cmd == QLatin1String("removebreakpoint")) { m_breakHandler->handleRemoveBreakpoint(&response, arguments); } else if (cmd == QLatin1String("echo")) { response.insert(QStringLiteral("result"), arguments); } else { for (NativeDebugger *debugger : qAsConst(m_debuggers)) if (debugger) debugger->handleCommand(&response, cmd, arguments); } QJsonDocument doc; doc.setObject(response); QByteArray ba = doc.toJson(QJsonDocument::Compact); TRACE_PROTOCOL("Sending synchronous response:" << ba.constData() << endl); emit messageToClient(s_key, ba); } void QQmlNativeDebugServiceImpl::emitAsynchronousMessageToClient(const QJsonObject &message) { QJsonDocument doc; doc.setObject(message); QByteArray ba = doc.toJson(QJsonDocument::Compact); TRACE_PROTOCOL("Sending asynchronous message:" << ba.constData() << endl); emit messageToClient(s_key, ba); } QT_END_NAMESPACE