/**************************************************************************** ** ** Copyright (C) 2015 The Qt Company Ltd. ** Contact: http://www.qt.io/licensing/ ** ** This file is part of the QtQml module of the Qt Toolkit. ** ** $QT_BEGIN_LICENSE:LGPL21$ ** 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 http://www.qt.io/terms-conditions. For further ** information use the contact form at http://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 2.1 or version 3 as published by the Free ** Software Foundation and appearing in the file LICENSE.LGPLv21 and ** LICENSE.LGPLv3 included in the packaging of this file. Please review the ** following information to ensure the GNU Lesser General Public License ** requirements will be met: https://www.gnu.org/licenses/lgpl.html and ** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. ** ** As a special exception, The Qt Company gives you certain additional ** rights. These rights are described in The Qt Company LGPL Exception ** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. ** ** $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 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(QStringLiteral("type")).toString(); QString fileName = arguments.value(QStringLiteral("file")).toString(); if (fileName.isEmpty()) { setError(response, QStringLiteral("breakpoint has no file name")); return; } int line = arguments.value(QStringLiteral("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(QStringLiteral("enabled")).toBool(true); bp.condition = arguments.value(QStringLiteral("condition")).toString(); bp.ignoreCount = arguments.value(QStringLiteral("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(QStringLiteral("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 Q_DECL_OVERRIDE { return m_pauseRequested || m_service->m_breakHandler->m_haveBreakPoints || m_stepping >= StepOver; } void maybeBreakAtInstruction() Q_DECL_OVERRIDE; void enteringFunction() Q_DECL_OVERRIDE; void leavingFunction(const QV4::ReturnedValue &retVal) Q_DECL_OVERRIDE; void aboutToThrow() Q_DECL_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(QV4::Scope &scope, 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::PersistentValue m_currentContext; 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::ReturnedValue result = evaluateExpression(scope, expression); QV4::ScopedValue val(scope, result); return val->booleanValue(); } QV4::ReturnedValue NativeDebugger::evaluateExpression(QV4::Scope &scope, const QString &expression) { m_runningJob = true; QV4::ExecutionContextSaver saver(scope); QV4::ExecutionContext *ctx = m_engine->currentContext; m_engine->pushContext(ctx); QV4::Script script(ctx, expression); script.strictMode = ctx->d()->strictMode; // 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(); QV4::ScopedValue result(scope); if (!m_engine->hasException) result = script.run(); m_runningJob = false; return result->asReturnedValue(); } 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(); foreach (const QString &signal, breakOnSignals) { if (signal == signalName) { // TODO: pause debugger break; } } } void NativeDebugger::handleCommand(QJsonObject *response, const QString &cmd, const QJsonObject &arguments) { if (cmd == QStringLiteral("backtrace")) handleBacktrace(response, arguments); else if (cmd == QStringLiteral("variables")) handleVariables(response, arguments); else if (cmd == QStringLiteral("expressions")) handleExpressions(response, arguments); else if (cmd == QStringLiteral("stepin")) handleContinue(response, StepIn); else if (cmd == QStringLiteral("stepout")) handleContinue(response, StepOut); else if (cmd == QStringLiteral("stepover")) handleContinue(response, StepOver); else if (cmd == QStringLiteral("continue")) handleContinue(response, NotStepping); } static QString encodeContext(QV4::ExecutionContext *executionContext) { QByteArray ba; QDataStream ds(&ba, QIODevice::WriteOnly); ds << quintptr(executionContext); return QString::fromLatin1(ba.toHex()); } static void decodeContext(const QString &context, QV4::ExecutionContext **executionContext) { quintptr rawContext; QDataStream ds(QByteArray::fromHex(context.toLatin1())); ds >> rawContext; *executionContext = reinterpret_cast(rawContext); } void NativeDebugger::handleBacktrace(QJsonObject *response, const QJsonObject &arguments) { int limit = arguments.value(QStringLiteral("limit")).toInt(0); QJsonArray frameArray; QV4::ExecutionContext *executionContext = m_engine->currentContext; for (int i = 0; i < limit && executionContext; ++i) { QV4::Heap::FunctionObject *heapFunctionObject = executionContext->getFunctionObject(); if (heapFunctionObject) { QJsonObject frame; frame[QStringLiteral("language")] = QStringLiteral("js"); frame[QStringLiteral("context")] = encodeContext(executionContext); if (QV4::Function *function = heapFunctionObject->function) { if (QV4::Heap::String *functionName = function->name()) frame[QStringLiteral("function")] = functionName->toQString(); frame[QStringLiteral("file")] = function->sourceFile(); } int line = executionContext->d()->lineNumber; frame[QStringLiteral("line")] = (line < 0 ? -line : line); frameArray.push_back(frame); } executionContext = m_engine->parentContext(executionContext); } 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::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::ExecutionContext *executionContext = 0; decodeContext(arguments.value(QStringLiteral("context")).toString(), &executionContext); if (!executionContext) { setError(response, QStringLiteral("No execution context passed")); return; } TRACE_PROTOCOL("Context: " << executionContext); QV4::ExecutionEngine *engine = executionContext->d()->engine; if (!engine) { setError(response, QStringLiteral("No execution engine passed")); return; } TRACE_PROTOCOL("Engine: " << engine); Collector collector(engine); QJsonArray expanded = arguments.value(QStringLiteral("expanded")).toArray(); foreach (const QJsonValue &ex, expanded) collector.m_expanded.append(ex.toString()); TRACE_PROTOCOL("Expanded: " << collector.m_expanded); QJsonArray output; QV4::Scope scope(engine); if (QV4::CallContext *callContext = executionContext->asCallContext()) { QV4::Value thisObject = callContext->thisObject(); collector.collect(&output, QString(), QStringLiteral("this"), thisObject); QV4::Identifier *const *variables = callContext->variables(); QV4::Identifier *const *formals = callContext->formals(); for (unsigned i = 0, ei = callContext->variableCount(); i != ei; ++i) { QString qName; if (QV4::Identifier *name = variables[i]) qName = name->string; QV4::Value val = callContext->d()->locals[i]; collector.collect(&output, QString(), qName, val); } for (unsigned i = 0, ei = callContext->formalCount(); i != ei; ++i) { QString qName; if (QV4::Identifier *name = formals[i]) qName = name->string; QV4::ReturnedValue rval = callContext->argument(i); QV4::ScopedValue sval(scope, rval); collector.collect(&output, QString(), qName, *sval); } } response->insert(QStringLiteral("variables"), output); } void NativeDebugger::handleExpressions(QJsonObject *response, const QJsonObject &arguments) { TRACE_PROTOCOL("Evaluate expressions"); QV4::ExecutionContext *executionContext = 0; decodeContext(arguments.value(QStringLiteral("context")).toString(), &executionContext); if (!executionContext) { setError(response, QStringLiteral("No execution context passed")); return; } TRACE_PROTOCOL("Context: " << executionContext); QV4::ExecutionEngine *engine = executionContext->d()->engine; if (!engine) { setError(response, QStringLiteral("No execution engine passed")); return; } TRACE_PROTOCOL("Engines: " << engine << m_engine); Collector collector(engine); QJsonArray expanded = arguments.value(QStringLiteral("expanded")).toArray(); foreach (const QJsonValue &ex, expanded) collector.m_expanded.append(ex.toString()); TRACE_PROTOCOL("Expanded: " << collector.m_expanded); QJsonArray output; QV4::Scope scope(engine); QJsonArray expressions = arguments.value(QStringLiteral("expressions")).toArray(); foreach (const QJsonValue &expr, expressions) { QString expression = expr.toObject().value(QStringLiteral("expression")).toString(); QString name = expr.toObject().value(QStringLiteral("name")).toString(); TRACE_PROTOCOL("Evaluate expression: " << expression); m_runningJob = true; QV4::ReturnedValue eval = evaluateExpression(scope, expression); QV4::ScopedValue result(scope, eval); m_runningJob = false; if (result->isUndefined()) { QJsonObject dict; dict[QStringLiteral("name")] = name; dict[QStringLiteral("valueencoded")] = QStringLiteral("undefined"); output.append(dict); } else if (result.ptr && result.ptr->_val) { collector.collect(&output, QString(), name, *result); } else { QJsonObject dict; dict[QStringLiteral("name")] = name; dict[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_currentContext.set(m_engine, *m_engine->currentContext); 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_currentContext.asManaged()->d() == m_engine->current) 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()) { const int lineNumber = m_engine->current->lineNumber; if (reallyHitTheBreakPoint(function, lineNumber)) pauseAndWait(); } } } void NativeDebugger::enteringFunction() { if (m_runningJob) return; if (m_stepping == StepIn) { m_currentContext.set(m_engine, *m_engine->currentContext); } } void NativeDebugger::leavingFunction(const QV4::ReturnedValue &retVal) { if (m_runningJob) return; if (m_stepping != NotStepping && m_currentContext.asManaged()->d() == m_engine->current) { m_currentContext.set(m_engine, *m_engine->parentContext(m_engine->currentContext)); 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 { QV4::Scope scope(m_engine); QV4::ExecutionContext *context = m_engine->currentContext; QV4::ScopedFunctionObject function(scope, context->getFunctionObject()); if (function) return function->function(); else return context->d()->engine->globalCode; } void NativeDebugger::pauseAndWait() { QJsonObject event; event.insert(QStringLiteral("event"), QStringLiteral("break")); event.insert(QStringLiteral("language"), QStringLiteral("js")); if (QV4::ExecutionContext *executionContext = m_engine->currentContext) { QV4::Heap::FunctionObject *heapFunctionObject = executionContext->getFunctionObject(); if (heapFunctionObject) { if (QV4::Function *function = heapFunctionObject->function) event.insert(QStringLiteral("file"), function->sourceFile()); int line = executionContext->d()->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 QString base = fileName.mid(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(QQmlEngine *engine) { TRACE_PROTOCOL("Adding engine" << engine); if (engine) { QV4::ExecutionEngine *ee = QV8Engine::getV4(engine->handle()); TRACE_PROTOCOL("Adding execution engine" << ee); if (ee) { NativeDebugger *debugger = new NativeDebugger(this, ee); ee->iselFactory.reset(new QV4::Moth::ISelFactory); if (state() == Enabled) ee->setDebugger(debugger); m_debuggers.append(QPointer(debugger)); } } QQmlDebugService::engineAboutToBeAdded(engine); } void QQmlNativeDebugServiceImpl::engineAboutToBeRemoved(QQmlEngine *engine) { TRACE_PROTOCOL("Removing engine" << engine); if (engine) { QV4::ExecutionEngine *executionEngine = QV8Engine::getV4(engine->handle()); foreach (NativeDebugger *debugger, m_debuggers) { if (debugger->engine() == executionEngine) m_debuggers.removeAll(debugger); } } QQmlDebugService::engineAboutToBeRemoved(engine); } void QQmlNativeDebugServiceImpl::stateAboutToBeChanged(QQmlDebugService::State state) { if (state == Enabled) { foreach (NativeDebugger *debugger, 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(QStringLiteral("arguments")).toObject(); QString cmd = request.value(QStringLiteral("command")).toString(); if (cmd == QStringLiteral("setbreakpoint")) { m_breakHandler->handleSetBreakpoint(&response, arguments); } else if (cmd == QStringLiteral("removebreakpoint")) { m_breakHandler->handleRemoveBreakpoint(&response, arguments); } else if (cmd == QStringLiteral("echo")) { response.insert(QStringLiteral("result"), arguments); } else { foreach (NativeDebugger *debugger, 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