/**************************************************************************** ** ** 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 "qv4debugservice.h" #include "qv4debugjob.h" #include "qqmlengine.h" #include #include #include #include #include #include #include #include const char *const V4_CONNECT = "connect"; const char *const V4_DISCONNECT = "disconnect"; const char *const V4_BREAK_ON_SIGNAL = "breakonsignal"; const char *const V4_PAUSE = "interrupt"; #define NO_PROTOCOL_TRACING #ifdef NO_PROTOCOL_TRACING # define TRACE_PROTOCOL(x) #else #include # define TRACE_PROTOCOL(x) x #endif QT_BEGIN_NAMESPACE class V4CommandHandler; class UnknownV4CommandHandler; using QQmlDebugPacket = QVersionedPacket; int QV4DebugServiceImpl::sequence = 0; class V4CommandHandler { public: V4CommandHandler(const QString &command) : cmd(command) {} virtual ~V4CommandHandler() {} QString command() const { return cmd; } void handle(const QJsonObject &request, QV4DebugServiceImpl *s) { TRACE_PROTOCOL(qDebug() << "handling command" << command() << "..."); req = request; seq = req.value(QLatin1String("seq")); debugService = s; handleRequest(); if (!response.isEmpty()) { response[QLatin1String("type")] = QStringLiteral("response"); debugService->send(response); } debugService = nullptr; seq = QJsonValue(); req = QJsonObject(); response = QJsonObject(); } virtual void handleRequest() = 0; protected: void addCommand() { response.insert(QStringLiteral("command"), cmd); } void addRequestSequence() { response.insert(QStringLiteral("request_seq"), seq); } void addSuccess(bool success) { response.insert(QStringLiteral("success"), success); } void addBody(const QJsonValue &body) { response.insert(QStringLiteral("body"), body); } void addRunning() { response.insert(QStringLiteral("running"), debugService->debuggerAgent.isRunning()); } void createErrorResponse(const QString &msg) { QJsonValue command = req.value(QLatin1String("command")); response.insert(QStringLiteral("command"), command); addRequestSequence(); addSuccess(false); addRunning(); response.insert(QStringLiteral("message"), msg); } int requestSequenceNr() const { return seq.toInt(-1); } protected: QString cmd; QJsonObject req; QJsonValue seq; QV4DebugServiceImpl *debugService; QJsonObject response; }; class UnknownV4CommandHandler: public V4CommandHandler { public: UnknownV4CommandHandler(): V4CommandHandler(QString()) {} void handleRequest() override { QString msg = QLatin1String("unimplemented command \"") + req.value(QLatin1String("command")).toString() + QLatin1Char('"'); createErrorResponse(msg); } }; namespace { class V4VersionRequest: public V4CommandHandler { public: V4VersionRequest(): V4CommandHandler(QStringLiteral("version")) {} void handleRequest() override { addCommand(); addRequestSequence(); addSuccess(true); addRunning(); QJsonObject body; body.insert(QStringLiteral("V8Version"), QLatin1String("this is not V8, this is V4 in Qt " QT_VERSION_STR)); body.insert(QStringLiteral("UnpausedEvaluate"), true); body.insert(QStringLiteral("ContextEvaluate"), true); body.insert(QStringLiteral("ChangeBreakpoint"), true); addBody(body); } }; class V4BreakPointRequest: public V4CommandHandler { public: V4BreakPointRequest(const QString &name): V4CommandHandler(name) {} void handleRequest() final { // Other types are currently not supported m_type = QStringLiteral("scriptRegExp"); // decypher the payload: m_args = req.value(QLatin1String("arguments")).toObject(); if (m_args.isEmpty()) { createErrorResponse(QStringLiteral("breakpoint request with empty arguments object")); return; } const int id = handleBreakPointRequest(); if (id < 0) { createErrorResponse(m_error); } else { // response: addCommand(); addRequestSequence(); addSuccess(true); addRunning(); QJsonObject body; body.insert(QStringLiteral("type"), m_type); body.insert(QStringLiteral("breakpoint"), id); addBody(body); } } protected: virtual int handleBreakPointRequest() = 0; QJsonObject m_args; QString m_type; QString m_error; }; class V4SetBreakPointRequest: public V4BreakPointRequest { public: V4SetBreakPointRequest(): V4BreakPointRequest(QStringLiteral("setbreakpoint")) {} int handleBreakPointRequest() final { // decypher the payload: const QString type = m_args.value(QLatin1String("type")).toString(); if (type != QLatin1String("scriptRegExp")) { m_error = QStringLiteral("breakpoint type \"%1\" is not implemented").arg(type); return -1; } const QString fileName = m_args.value(QLatin1String("target")).toString(); if (fileName.isEmpty()) { m_error = QStringLiteral("breakpoint has no file name"); return -1; } const int line = m_args.value(QLatin1String("line")).toInt(-1); if (line < 0) { m_error = QStringLiteral("breakpoint has an invalid line number"); return -1; } const bool enabled = m_args.value(QStringLiteral("enabled")).toBool(true); const QString condition = m_args.value(QStringLiteral("condition")).toString(); // set the break point: return debugService->debuggerAgent.addBreakPoint(fileName, line + 1, enabled, condition); // It's undocumented, but V8 sends back an actual_locations array too. However, our // Debugger currently doesn't tell us when it resolved a breakpoint, so we'll leave them // pending until the breakpoint is hit for the first time. } }; class V4ClearBreakPointRequest: public V4BreakPointRequest { public: V4ClearBreakPointRequest(): V4BreakPointRequest(QStringLiteral("clearbreakpoint")) {} int handleBreakPointRequest() final { const int id = m_args.value(QLatin1String("breakpoint")).toInt(-1); if (id < 0) m_error = QStringLiteral("breakpoint has an invalid number"); else // remove the break point: debugService->debuggerAgent.removeBreakPoint(id); return id; } }; class V4ChangeBreakPointRequest: public V4BreakPointRequest { public: V4ChangeBreakPointRequest(): V4BreakPointRequest(QStringLiteral("changebreakpoint")) {} int handleBreakPointRequest() final { const int id = m_args.value(QLatin1String("breakpoint")).toInt(-1); if (id < 0) { m_error = QStringLiteral("breakpoint has an invalid number"); return id; } const QJsonValue enabled = m_args.value(QLatin1String("enabled")); if (!enabled.isBool()) { m_error = QStringLiteral("missing bool \"enabled\" in breakpoint change request"); return -1; } // enable or disable the break point: debugService->debuggerAgent.enableBreakPoint(id, enabled.toBool()); return id; } }; class V4BacktraceRequest: public V4CommandHandler { public: V4BacktraceRequest(): V4CommandHandler(QStringLiteral("backtrace")) {} void handleRequest() override { // decypher the payload: QJsonObject arguments = req.value(QLatin1String("arguments")).toObject(); int fromFrame = arguments.value(QLatin1String("fromFrame")).toInt(0); int toFrame = arguments.value(QLatin1String("toFrame")).toInt(fromFrame + 10); // no idea what the bottom property is for, so we'll ignore it. QV4Debugger *debugger = debugService->debuggerAgent.pausedDebugger(); if (!debugger) { createErrorResponse(QStringLiteral("Debugger has to be paused to retrieve backtraces.")); return; } BacktraceJob job(debugger->collector(), fromFrame, toFrame); debugger->runInEngine(&job); // response: addCommand(); addRequestSequence(); addSuccess(true); addRunning(); addBody(job.returnValue()); } }; class V4FrameRequest: public V4CommandHandler { public: V4FrameRequest(): V4CommandHandler(QStringLiteral("frame")) {} void handleRequest() override { // decypher the payload: QJsonObject arguments = req.value(QLatin1String("arguments")).toObject(); const int frameNr = arguments.value(QLatin1String("number")).toInt( debugService->selectedFrame()); QV4Debugger *debugger = debugService->debuggerAgent.pausedDebugger(); if (!debugger) { createErrorResponse(QStringLiteral("Debugger has to be paused to retrieve frames.")); return; } if (frameNr < 0) { createErrorResponse(QStringLiteral("frame command has invalid frame number")); return; } FrameJob job(debugger->collector(), frameNr); debugger->runInEngine(&job); if (!job.wasSuccessful()) { createErrorResponse(QStringLiteral("frame retrieval failed")); return; } debugService->selectFrame(frameNr); // response: addCommand(); addRequestSequence(); addSuccess(true); addRunning(); addBody(job.returnValue()); } }; class V4ScopeRequest: public V4CommandHandler { public: V4ScopeRequest(): V4CommandHandler(QStringLiteral("scope")) {} void handleRequest() override { // decypher the payload: QJsonObject arguments = req.value(QLatin1String("arguments")).toObject(); const int frameNr = arguments.value(QLatin1String("frameNumber")).toInt( debugService->selectedFrame()); const int scopeNr = arguments.value(QLatin1String("number")).toInt(0); QV4Debugger *debugger = debugService->debuggerAgent.pausedDebugger(); if (!debugger) { createErrorResponse(QStringLiteral("Debugger has to be paused to retrieve scope.")); return; } if (frameNr < 0) { createErrorResponse(QStringLiteral("scope command has invalid frame number")); return; } if (scopeNr < 0) { createErrorResponse(QStringLiteral("scope command has invalid scope number")); return; } ScopeJob job(debugger->collector(), frameNr, scopeNr); debugger->runInEngine(&job); if (!job.wasSuccessful()) { createErrorResponse(QStringLiteral("scope retrieval failed")); return; } // response: addCommand(); addRequestSequence(); addSuccess(true); addRunning(); addBody(job.returnValue()); } }; class V4LookupRequest: public V4CommandHandler { public: V4LookupRequest(): V4CommandHandler(QStringLiteral("lookup")) {} void handleRequest() override { // decypher the payload: QJsonObject arguments = req.value(QLatin1String("arguments")).toObject(); QJsonArray handles = arguments.value(QLatin1String("handles")).toArray(); QV4Debugger *debugger = debugService->debuggerAgent.pausedDebugger(); if (!debugger) { const QList &debuggers = debugService->debuggerAgent.debuggers(); if (debuggers.count() > 1) { createErrorResponse(QStringLiteral("Cannot lookup values if multiple debuggers are running and none is paused")); return; } else if (debuggers.count() == 0) { createErrorResponse(QStringLiteral("No debuggers available to lookup values")); return; } debugger = debuggers.first(); } ValueLookupJob job(handles, debugger->collector()); debugger->runInEngine(&job); if (!job.exceptionMessage().isEmpty()) { createErrorResponse(job.exceptionMessage()); } else { // response: addCommand(); addRequestSequence(); addSuccess(true); addRunning(); addBody(job.returnValue()); } } }; class V4ContinueRequest: public V4CommandHandler { public: V4ContinueRequest(): V4CommandHandler(QStringLiteral("continue")) {} void handleRequest() override { // decypher the payload: QJsonObject arguments = req.value(QLatin1String("arguments")).toObject(); QV4Debugger *debugger = debugService->debuggerAgent.pausedDebugger(); if (!debugger) { createErrorResponse(QStringLiteral("Debugger has to be paused in order to continue.")); return; } debugService->debuggerAgent.clearAllPauseRequests(); if (arguments.empty()) { debugger->resume(QV4Debugger::FullThrottle); } else { QJsonObject arguments = req.value(QLatin1String("arguments")).toObject(); QString stepAction = arguments.value(QLatin1String("stepaction")).toString(); const int stepcount = arguments.value(QLatin1String("stepcount")).toInt(1); if (stepcount != 1) qWarning() << "Step count other than 1 is not supported."; if (stepAction == QLatin1String("in")) { debugger->resume(QV4Debugger::StepIn); } else if (stepAction == QLatin1String("out")) { debugger->resume(QV4Debugger::StepOut); } else if (stepAction == QLatin1String("next")) { debugger->resume(QV4Debugger::StepOver); } else { createErrorResponse(QStringLiteral("continue command has invalid stepaction")); return; } } // response: addCommand(); addRequestSequence(); addSuccess(true); addRunning(); } }; class V4DisconnectRequest: public V4CommandHandler { public: V4DisconnectRequest(): V4CommandHandler(QStringLiteral("disconnect")) {} void handleRequest() override { debugService->debuggerAgent.removeAllBreakPoints(); debugService->debuggerAgent.resumeAll(); // response: addCommand(); addRequestSequence(); addSuccess(true); addRunning(); } }; class V4SetExceptionBreakRequest: public V4CommandHandler { public: V4SetExceptionBreakRequest(): V4CommandHandler(QStringLiteral("setexceptionbreak")) {} void handleRequest() override { bool wasEnabled = debugService->debuggerAgent.breakOnThrow(); //decypher the payload: QJsonObject arguments = req.value(QLatin1String("arguments")).toObject(); QString type = arguments.value(QLatin1String("type")).toString(); bool enabled = arguments.value(QLatin1String("number")).toBool(!wasEnabled); if (type == QLatin1String("all")) { // that's fine } else if (type == QLatin1String("uncaught")) { createErrorResponse(QStringLiteral("breaking only on uncaught exceptions is not supported yet")); return; } else { createErrorResponse(QStringLiteral("invalid type for break on exception")); return; } // do it: debugService->debuggerAgent.setBreakOnThrow(enabled); QJsonObject body; body[QLatin1String("type")] = type; body[QLatin1String("enabled")] = debugService->debuggerAgent.breakOnThrow(); // response: addBody(body); addRunning(); addSuccess(true); addRequestSequence(); addCommand(); } }; class V4ScriptsRequest: public V4CommandHandler { public: V4ScriptsRequest(): V4CommandHandler(QStringLiteral("scripts")) {} void handleRequest() override { //decypher the payload: QJsonObject arguments = req.value(QLatin1String("arguments")).toObject(); int types = arguments.value(QLatin1String("types")).toInt(-1); if (types < 0 || types > 7) { createErrorResponse(QStringLiteral("invalid types value in scripts command")); return; } else if (types != 4) { createErrorResponse(QStringLiteral("unsupported types value in scripts command")); return; } // do it: QV4Debugger *debugger = debugService->debuggerAgent.pausedDebugger(); if (!debugger) { createErrorResponse(QStringLiteral("Debugger has to be paused to retrieve scripts.")); return; } GatherSourcesJob job(debugger->engine()); debugger->runInEngine(&job); QJsonArray body; for (const QString &source : job.result()) { QJsonObject src; src[QLatin1String("name")] = source; src[QLatin1String("scriptType")] = 4; body.append(src); } addSuccess(true); addRunning(); addBody(body); addCommand(); addRequestSequence(); } }; // Request: // { // "seq": 4, // "type": "request", // "command": "evaluate", // "arguments": { // "expression": "a", // "frame": 0 // } // } // // Response: // { // "body": { // "handle": 3, // "type": "number", // "value": 1 // }, // "command": "evaluate", // "refs": [], // "request_seq": 4, // "running": false, // "seq": 5, // "success": true, // "type": "response" // } // // The "value" key in "body" is the result of evaluating the expression in the request. class V4EvaluateRequest: public V4CommandHandler { public: V4EvaluateRequest(): V4CommandHandler(QStringLiteral("evaluate")) {} void handleRequest() override { QJsonObject arguments = req.value(QLatin1String("arguments")).toObject(); QString expression = arguments.value(QLatin1String("expression")).toString(); int context = arguments.value(QLatin1String("context")).toInt(-1); int frame = -1; QV4Debugger *debugger = debugService->debuggerAgent.pausedDebugger(); if (!debugger) { const QList &debuggers = debugService->debuggerAgent.debuggers(); if (debuggers.count() > 1) { createErrorResponse(QStringLiteral("Cannot evaluate expressions if multiple debuggers are running and none is paused")); return; } else if (debuggers.count() == 0) { createErrorResponse(QStringLiteral("No debuggers available to evaluate expressions")); return; } debugger = debuggers.first(); } else { frame = arguments.value(QLatin1String("frame")).toInt(0); } ExpressionEvalJob job(debugger->engine(), frame, context, expression, debugger->collector()); debugger->runInEngine(&job); if (job.hasExeption()) { createErrorResponse(job.exceptionMessage()); } else { addCommand(); addRequestSequence(); addSuccess(true); addRunning(); addBody(job.returnValue()); } } }; } // anonymous namespace void QV4DebugServiceImpl::addHandler(V4CommandHandler* handler) { handlers[handler->command()] = handler; } V4CommandHandler *QV4DebugServiceImpl::v4CommandHandler(const QString &command) const { V4CommandHandler *handler = handlers.value(command, 0); if (handler) return handler; else return unknownV4CommandHandler.data(); } QV4DebugServiceImpl::QV4DebugServiceImpl(QObject *parent) : QQmlConfigurableDebugService(1, parent), debuggerAgent(this), theSelectedFrame(0), unknownV4CommandHandler(new UnknownV4CommandHandler) { addHandler(new V4VersionRequest); addHandler(new V4SetBreakPointRequest); addHandler(new V4ClearBreakPointRequest); addHandler(new V4ChangeBreakPointRequest); addHandler(new V4BacktraceRequest); addHandler(new V4FrameRequest); addHandler(new V4ScopeRequest); addHandler(new V4LookupRequest); addHandler(new V4ContinueRequest); addHandler(new V4DisconnectRequest); addHandler(new V4SetExceptionBreakRequest); addHandler(new V4ScriptsRequest); addHandler(new V4EvaluateRequest); } QV4DebugServiceImpl::~QV4DebugServiceImpl() { qDeleteAll(handlers); } void QV4DebugServiceImpl::engineAdded(QJSEngine *engine) { QMutexLocker lock(&m_configMutex); if (engine) { QV4::ExecutionEngine *ee = engine->handle(); if (QQmlDebugConnector *server = QQmlDebugConnector::instance()) { if (ee) { QV4Debugger *debugger = new QV4Debugger(ee); if (state() == Enabled) ee->setDebugger(debugger); debuggerAgent.addDebugger(debugger); debuggerAgent.moveToThread(server->thread()); } } } QQmlConfigurableDebugService::engineAdded(engine); } void QV4DebugServiceImpl::engineAboutToBeRemoved(QJSEngine *engine) { QMutexLocker lock(&m_configMutex); if (engine){ const QV4::ExecutionEngine *ee = engine->handle(); if (ee) { QV4Debugger *debugger = qobject_cast(ee->debugger()); if (debugger) debuggerAgent.removeDebugger(debugger); } } QQmlConfigurableDebugService::engineAboutToBeRemoved(engine); } void QV4DebugServiceImpl::stateAboutToBeChanged(State state) { QMutexLocker lock(&m_configMutex); if (state == Enabled) { const auto debuggers = debuggerAgent.debuggers(); for (QV4Debugger *debugger : debuggers) { QV4::ExecutionEngine *ee = debugger->engine(); if (!ee->debugger()) ee->setDebugger(debugger); } } QQmlConfigurableDebugService::stateAboutToBeChanged(state); } void QV4DebugServiceImpl::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 QV4DebugServiceImpl::messageReceived(const QByteArray &message) { QMutexLocker lock(&m_configMutex); QQmlDebugPacket ms(message); QByteArray header; ms >> header; TRACE_PROTOCOL(qDebug() << "received message with header" << header); if (header == "V8DEBUG") { QByteArray type; QByteArray payload; ms >> type >> payload; TRACE_PROTOCOL(qDebug() << "... type:" << type); if (type == V4_CONNECT) { QJsonObject parameters = QJsonDocument::fromJson(payload).object(); Q_UNUSED(parameters); // For future protocol changes emit messageToClient(name(), packMessage(type)); stopWaiting(); } else if (type == V4_PAUSE) { debuggerAgent.pauseAll(); sendSomethingToSomebody(type); } else if (type == V4_BREAK_ON_SIGNAL) { QByteArray signal; bool enabled; ms >> signal >> enabled; //Normalize to lower case. QString signalName(QString::fromUtf8(signal).toLower()); if (enabled) breakOnSignals.append(signalName); else breakOnSignals.removeOne(signalName); } else if (type == "v8request") { handleV4Request(payload); } else if (type == V4_DISCONNECT) { TRACE_PROTOCOL(qDebug() << "... payload:" << payload.constData()); handleV4Request(payload); } else { sendSomethingToSomebody(type, 0); } } } void QV4DebugServiceImpl::sendSomethingToSomebody(const char *type, int magicNumber) { QQmlDebugPacket rs; rs << QByteArray(type) << QByteArray::number(int(version())) << QByteArray::number(magicNumber); emit messageToClient(name(), packMessage(type, rs.data())); } void QV4DebugServiceImpl::handleV4Request(const QByteArray &payload) { TRACE_PROTOCOL(qDebug() << "v8request, payload:" << payload.constData()); QJsonDocument request = QJsonDocument::fromJson(payload); QJsonObject o = request.object(); QJsonValue type = o.value(QLatin1String("type")); if (type.toString() == QLatin1String("request")) { QJsonValue command = o.value(QLatin1String("command")); V4CommandHandler *h = v4CommandHandler(command.toString()); if (h) h->handle(o, this); } } QByteArray QV4DebugServiceImpl::packMessage(const QByteArray &command, const QByteArray &message) { QQmlDebugPacket rs; static const QByteArray cmd("V8DEBUG"); rs << cmd << command << message; return rs.data(); } void QV4DebugServiceImpl::send(QJsonObject v4Payload) { v4Payload[QLatin1String("seq")] = sequence++; QJsonDocument doc; doc.setObject(v4Payload); #ifdef NO_PROTOCOL_TRACING QByteArray responseData = doc.toJson(QJsonDocument::Compact); #else QByteArray responseData = doc.toJson(QJsonDocument::Indented); #endif TRACE_PROTOCOL(qDebug() << "sending response for:" << responseData.constData() << endl); emit messageToClient(name(), packMessage("v8message", responseData)); } void QV4DebugServiceImpl::selectFrame(int frameNr) { theSelectedFrame = frameNr; } int QV4DebugServiceImpl::selectedFrame() const { return theSelectedFrame; } QT_END_NAMESPACE #include "moc_qv4debugservice.cpp"