| 1 | // Copyright (C) 2016 The Qt Company Ltd. |
| 2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only |
| 3 | |
| 4 | #include "qqmlnativedebugservice.h" |
| 5 | |
| 6 | #include <private/qqmldebugconnector_p.h> |
| 7 | #include <private/qqmldebugserviceinterfaces_p.h> |
| 8 | #include <private/qv4debugging_p.h> |
| 9 | #include <private/qv4debugging_p.h> |
| 10 | #include <private/qv4engine_p.h> |
| 11 | #include <private/qv4identifierhash_p.h> |
| 12 | #include <private/qv4identifiertable_p.h> |
| 13 | #include <private/qv4objectiterator_p.h> |
| 14 | #include <private/qv4runtime_p.h> |
| 15 | #include <private/qv4script_p.h> |
| 16 | #include <private/qv4stackframe_p.h> |
| 17 | #include <private/qv4string_p.h> |
| 18 | #include <private/qversionedpacket_p.h> |
| 19 | |
| 20 | #include <QtQml/qjsengine.h> |
| 21 | #include <QtCore/qjsonarray.h> |
| 22 | #include <QtCore/qjsondocument.h> |
| 23 | #include <QtCore/qjsonobject.h> |
| 24 | #include <QtCore/qjsonvalue.h> |
| 25 | #include <QtCore/qvector.h> |
| 26 | #include <QtCore/qpointer.h> |
| 27 | |
| 28 | //#define TRACE_PROTOCOL(s) qDebug() << s |
| 29 | #define TRACE_PROTOCOL(s) |
| 30 | |
| 31 | QT_BEGIN_NAMESPACE |
| 32 | |
| 33 | using QQmlDebugPacket = QVersionedPacket<QQmlDebugConnector>; |
| 34 | |
| 35 | class BreakPoint |
| 36 | { |
| 37 | public: |
| 38 | BreakPoint() : id(-1), lineNumber(-1), enabled(false), ignoreCount(0), hitCount(0) {} |
| 39 | bool isValid() const { return lineNumber >= 0 && !fileName.isEmpty(); } |
| 40 | |
| 41 | int id; |
| 42 | int lineNumber; |
| 43 | QString fileName; |
| 44 | bool enabled; |
| 45 | QString condition; |
| 46 | int ignoreCount; |
| 47 | |
| 48 | int hitCount; |
| 49 | }; |
| 50 | |
| 51 | inline size_t qHash(const BreakPoint &b, size_t seed = 0) noexcept |
| 52 | { |
| 53 | return qHash(key: b.fileName, seed) ^ b.lineNumber; |
| 54 | } |
| 55 | |
| 56 | inline bool operator==(const BreakPoint &a, const BreakPoint &b) |
| 57 | { |
| 58 | return a.lineNumber == b.lineNumber && a.fileName == b.fileName |
| 59 | && a.enabled == b.enabled && a.condition == b.condition |
| 60 | && a.ignoreCount == b.ignoreCount; |
| 61 | } |
| 62 | |
| 63 | static void setError(QJsonObject *response, const QString &msg) |
| 64 | { |
| 65 | response->insert(QStringLiteral("type" ), QStringLiteral("error" )); |
| 66 | response->insert(QStringLiteral("msg" ), value: msg); |
| 67 | } |
| 68 | |
| 69 | class NativeDebugger; |
| 70 | |
| 71 | class Collector |
| 72 | { |
| 73 | public: |
| 74 | Collector(QV4::ExecutionEngine *engine) |
| 75 | : m_engine(engine), m_anonCount(0) |
| 76 | {} |
| 77 | |
| 78 | void collect(QJsonArray *output, const QString &parentIName, const QString &name, |
| 79 | const QV4::Value &value); |
| 80 | |
| 81 | bool isExpanded(const QString &iname) const { return m_expanded.contains(str: iname); } |
| 82 | |
| 83 | public: |
| 84 | QV4::ExecutionEngine *m_engine; |
| 85 | int m_anonCount; |
| 86 | QStringList m_expanded; |
| 87 | }; |
| 88 | |
| 89 | // Encapsulate Breakpoint handling |
| 90 | // Could be made per-NativeDebugger (i.e. per execution engine, if needed) |
| 91 | class BreakPointHandler |
| 92 | { |
| 93 | public: |
| 94 | BreakPointHandler() : m_haveBreakPoints(false), m_breakOnThrow(true), m_lastBreakpoint(1) {} |
| 95 | |
| 96 | void handleSetBreakpoint(QJsonObject *response, const QJsonObject &arguments); |
| 97 | void handleRemoveBreakpoint(QJsonObject *response, const QJsonObject &arguments); |
| 98 | |
| 99 | void removeBreakPoint(int id); |
| 100 | void enableBreakPoint(int id, bool onoff); |
| 101 | |
| 102 | void setBreakOnThrow(bool onoff); |
| 103 | bool m_haveBreakPoints; |
| 104 | bool m_breakOnThrow; |
| 105 | int m_lastBreakpoint; |
| 106 | QVector<BreakPoint> m_breakPoints; |
| 107 | }; |
| 108 | |
| 109 | void BreakPointHandler::handleSetBreakpoint(QJsonObject *response, const QJsonObject &arguments) |
| 110 | { |
| 111 | TRACE_PROTOCOL("SET BREAKPOINT" << arguments); |
| 112 | QString type = arguments.value(key: QLatin1String("type" )).toString(); |
| 113 | |
| 114 | QString fileName = arguments.value(key: QLatin1String("file" )).toString(); |
| 115 | if (fileName.isEmpty()) { |
| 116 | setError(response, QStringLiteral("breakpoint has no file name" )); |
| 117 | return; |
| 118 | } |
| 119 | |
| 120 | int line = arguments.value(key: QLatin1String("line" )).toInt(defaultValue: -1); |
| 121 | if (line < 0) { |
| 122 | setError(response, QStringLiteral("breakpoint has an invalid line number" )); |
| 123 | return; |
| 124 | } |
| 125 | |
| 126 | BreakPoint bp; |
| 127 | bp.id = m_lastBreakpoint++; |
| 128 | bp.fileName = fileName.mid(position: fileName.lastIndexOf(c: '/') + 1); |
| 129 | bp.lineNumber = line; |
| 130 | bp.enabled = arguments.value(key: QLatin1String("enabled" )).toBool(defaultValue: true); |
| 131 | bp.condition = arguments.value(key: QLatin1String("condition" )).toString(); |
| 132 | bp.ignoreCount = arguments.value(key: QLatin1String("ignorecount" )).toInt(); |
| 133 | m_breakPoints.append(t: bp); |
| 134 | |
| 135 | m_haveBreakPoints = true; |
| 136 | |
| 137 | response->insert(QStringLiteral("type" ), value: type); |
| 138 | response->insert(QStringLiteral("breakpoint" ), value: bp.id); |
| 139 | } |
| 140 | |
| 141 | void BreakPointHandler::handleRemoveBreakpoint(QJsonObject *response, const QJsonObject &arguments) |
| 142 | { |
| 143 | int id = arguments.value(key: QLatin1String("id" )).toInt(); |
| 144 | removeBreakPoint(id); |
| 145 | response->insert(QStringLiteral("id" ), value: id); |
| 146 | } |
| 147 | |
| 148 | class NativeDebugger : public QV4::Debugging::Debugger |
| 149 | { |
| 150 | public: |
| 151 | NativeDebugger(QQmlNativeDebugServiceImpl *service, QV4::ExecutionEngine *engine); |
| 152 | |
| 153 | void signalEmitted(const QString &signal); |
| 154 | |
| 155 | QV4::ExecutionEngine *engine() const { return m_engine; } |
| 156 | |
| 157 | bool pauseAtNextOpportunity() const override { |
| 158 | return m_pauseRequested |
| 159 | || m_service->m_breakHandler->m_haveBreakPoints |
| 160 | || m_stepping >= StepOver; |
| 161 | } |
| 162 | |
| 163 | void maybeBreakAtInstruction() override; |
| 164 | void enteringFunction() override; |
| 165 | void leavingFunction(const QV4::ReturnedValue &retVal) override; |
| 166 | void aboutToThrow() override; |
| 167 | |
| 168 | void handleCommand(QJsonObject *response, const QString &cmd, const QJsonObject &arguments); |
| 169 | |
| 170 | private: |
| 171 | void handleBacktrace(QJsonObject *response, const QJsonObject &arguments); |
| 172 | void handleVariables(QJsonObject *response, const QJsonObject &arguments); |
| 173 | void handleExpressions(QJsonObject *response, const QJsonObject &arguments); |
| 174 | |
| 175 | void handleDebuggerDeleted(QObject *debugger); |
| 176 | |
| 177 | QV4::ReturnedValue evaluateExpression(const QString &expression); |
| 178 | bool checkCondition(const QString &expression); |
| 179 | |
| 180 | QStringList breakOnSignals; |
| 181 | |
| 182 | enum Speed { |
| 183 | NotStepping = 0, |
| 184 | StepOut, |
| 185 | StepOver, |
| 186 | StepIn, |
| 187 | }; |
| 188 | |
| 189 | void pauseAndWait(); |
| 190 | void pause(); |
| 191 | void handleContinue(QJsonObject *reponse, Speed speed); |
| 192 | |
| 193 | QV4::Function *getFunction() const; |
| 194 | |
| 195 | bool reallyHitTheBreakPoint(const QV4::Function *function, int lineNumber); |
| 196 | |
| 197 | QV4::ExecutionEngine *m_engine; |
| 198 | QQmlNativeDebugServiceImpl *m_service; |
| 199 | QV4::CppStackFrame *m_currentFrame = nullptr; |
| 200 | Speed m_stepping; |
| 201 | bool m_pauseRequested; |
| 202 | bool m_runningJob; |
| 203 | |
| 204 | QV4::PersistentValue m_returnedValue; |
| 205 | }; |
| 206 | |
| 207 | bool NativeDebugger::checkCondition(const QString &expression) |
| 208 | { |
| 209 | QV4::Scope scope(m_engine); |
| 210 | QV4::ScopedValue r(scope, evaluateExpression(expression)); |
| 211 | return r->booleanValue(); |
| 212 | } |
| 213 | |
| 214 | QV4::ReturnedValue NativeDebugger::evaluateExpression(const QString &expression) |
| 215 | { |
| 216 | QV4::Scope scope(m_engine); |
| 217 | m_runningJob = true; |
| 218 | |
| 219 | QV4::ExecutionContext *ctx = m_engine->currentStackFrame ? m_engine->currentContext() |
| 220 | : m_engine->scriptContext(); |
| 221 | |
| 222 | QV4::Script script(ctx, QV4::Compiler::ContextType::Eval, expression); |
| 223 | if (const QV4::Function *function = m_engine->currentStackFrame |
| 224 | ? m_engine->currentStackFrame->v4Function : m_engine->globalCode) |
| 225 | script.strictMode = function->isStrict(); |
| 226 | // In order for property lookups in QML to work, we need to disable fast v4 lookups. |
| 227 | // That is a side-effect of inheritContext. |
| 228 | script.inheritContext = true; |
| 229 | script.parse(); |
| 230 | if (!m_engine->hasException) { |
| 231 | if (m_engine->currentStackFrame) { |
| 232 | QV4::ScopedValue thisObject(scope, m_engine->currentStackFrame->thisObject()); |
| 233 | script.run(thisObject); |
| 234 | } else { |
| 235 | script.run(); |
| 236 | } |
| 237 | } |
| 238 | |
| 239 | m_runningJob = false; |
| 240 | return QV4::Encode::undefined(); |
| 241 | } |
| 242 | |
| 243 | NativeDebugger::NativeDebugger(QQmlNativeDebugServiceImpl *service, QV4::ExecutionEngine *engine) |
| 244 | : m_returnedValue(engine, QV4::Value::undefinedValue()) |
| 245 | { |
| 246 | m_stepping = NotStepping; |
| 247 | m_pauseRequested = false; |
| 248 | m_runningJob = false; |
| 249 | m_service = service; |
| 250 | m_engine = engine; |
| 251 | TRACE_PROTOCOL("Creating native debugger" ); |
| 252 | } |
| 253 | |
| 254 | void NativeDebugger::signalEmitted(const QString &signal) |
| 255 | { |
| 256 | //This function is only called by QQmlBoundSignal |
| 257 | //only if there is a slot connected to the signal. Hence, there |
| 258 | //is no need for additional check. |
| 259 | |
| 260 | //Parse just the name and remove the class info |
| 261 | //Normalize to Lower case. |
| 262 | QString signalName = signal.left(n: signal.indexOf(ch: QLatin1Char('('))).toLower(); |
| 263 | |
| 264 | for (const QString &signal : std::as_const(t&: breakOnSignals)) { |
| 265 | if (signal == signalName) { |
| 266 | // TODO: pause debugger |
| 267 | break; |
| 268 | } |
| 269 | } |
| 270 | } |
| 271 | |
| 272 | void NativeDebugger::handleCommand(QJsonObject *response, const QString &cmd, |
| 273 | const QJsonObject &arguments) |
| 274 | { |
| 275 | if (cmd == QLatin1String("backtrace" )) |
| 276 | handleBacktrace(response, arguments); |
| 277 | else if (cmd == QLatin1String("variables" )) |
| 278 | handleVariables(response, arguments); |
| 279 | else if (cmd == QLatin1String("expressions" )) |
| 280 | handleExpressions(response, arguments); |
| 281 | else if (cmd == QLatin1String("stepin" )) |
| 282 | handleContinue(reponse: response, speed: StepIn); |
| 283 | else if (cmd == QLatin1String("stepout" )) |
| 284 | handleContinue(reponse: response, speed: StepOut); |
| 285 | else if (cmd == QLatin1String("stepover" )) |
| 286 | handleContinue(reponse: response, speed: StepOver); |
| 287 | else if (cmd == QLatin1String("continue" )) |
| 288 | handleContinue(reponse: response, speed: NotStepping); |
| 289 | } |
| 290 | |
| 291 | static QString encodeFrame(QV4::CppStackFrame *f) |
| 292 | { |
| 293 | QQmlDebugPacket ds; |
| 294 | ds << quintptr(f); |
| 295 | return QString::fromLatin1(ba: ds.data().toHex()); |
| 296 | } |
| 297 | |
| 298 | static void decodeFrame(const QString &f, QV4::CppStackFrame **frame) |
| 299 | { |
| 300 | quintptr rawFrame; |
| 301 | QQmlDebugPacket ds(QByteArray::fromHex(hexEncoded: f.toLatin1())); |
| 302 | ds >> rawFrame; |
| 303 | *frame = reinterpret_cast<QV4::CppStackFrame *>(rawFrame); |
| 304 | } |
| 305 | |
| 306 | void NativeDebugger::handleBacktrace(QJsonObject *response, const QJsonObject &arguments) |
| 307 | { |
| 308 | int limit = arguments.value(key: QLatin1String("limit" )).toInt(defaultValue: 0); |
| 309 | |
| 310 | QJsonArray frameArray; |
| 311 | QV4::CppStackFrame *f= m_engine->currentStackFrame; |
| 312 | for (int i = 0; i < limit && f; ++i) { |
| 313 | QV4::Function *function = f->v4Function; |
| 314 | |
| 315 | QJsonObject frame; |
| 316 | frame.insert(QStringLiteral("language" ), QStringLiteral("js" )); |
| 317 | frame.insert(QStringLiteral("context" ), value: encodeFrame(f)); |
| 318 | |
| 319 | if (QV4::Heap::String *functionName = function->name()) |
| 320 | frame.insert(QStringLiteral("function" ), value: functionName->toQString()); |
| 321 | frame.insert(QStringLiteral("file" ), value: function->sourceFile()); |
| 322 | |
| 323 | int line = f->lineNumber(); |
| 324 | frame.insert(QStringLiteral("line" ), value: (line < 0 ? -line : line)); |
| 325 | |
| 326 | frameArray.push_back(t: frame); |
| 327 | |
| 328 | f = f->parentFrame(); |
| 329 | } |
| 330 | |
| 331 | response->insert(QStringLiteral("frames" ), value: frameArray); |
| 332 | } |
| 333 | |
| 334 | void Collector::collect(QJsonArray *out, const QString &parentIName, const QString &name, |
| 335 | const QV4::Value &value) |
| 336 | { |
| 337 | QJsonObject dict; |
| 338 | QV4::Scope scope(m_engine); |
| 339 | |
| 340 | QString nonEmptyName = name.isEmpty() ? QString::fromLatin1(ba: "@%1" ).arg(a: m_anonCount++) : name; |
| 341 | QString iname = parentIName + QLatin1Char('.') + nonEmptyName; |
| 342 | dict.insert(QStringLiteral("iname" ), value: iname); |
| 343 | dict.insert(QStringLiteral("name" ), value: nonEmptyName); |
| 344 | |
| 345 | QV4::ScopedValue typeString(scope, QV4::Runtime::TypeofValue::call(m_engine, value)); |
| 346 | dict.insert(QStringLiteral("type" ), value: typeString->toQStringNoThrow()); |
| 347 | |
| 348 | switch (value.type()) { |
| 349 | case QV4::Value::Empty_Type: |
| 350 | dict.insert(QStringLiteral("valueencoded" ), QStringLiteral("empty" )); |
| 351 | dict.insert(QStringLiteral("haschild" ), value: false); |
| 352 | break; |
| 353 | case QV4::Value::Undefined_Type: |
| 354 | dict.insert(QStringLiteral("valueencoded" ), QStringLiteral("undefined" )); |
| 355 | dict.insert(QStringLiteral("haschild" ), value: false); |
| 356 | break; |
| 357 | case QV4::Value::Null_Type: |
| 358 | dict.insert(QStringLiteral("type" ), QStringLiteral("object" )); |
| 359 | dict.insert(QStringLiteral("valueencoded" ), QStringLiteral("null" )); |
| 360 | dict.insert(QStringLiteral("haschild" ), value: false); |
| 361 | break; |
| 362 | case QV4::Value::Boolean_Type: |
| 363 | dict.insert(QStringLiteral("value" ), value: value.booleanValue()); |
| 364 | dict.insert(QStringLiteral("haschild" ), value: false); |
| 365 | break; |
| 366 | case QV4::Value::Managed_Type: |
| 367 | if (const QV4::String *string = value.as<QV4::String>()) { |
| 368 | dict.insert(QStringLiteral("value" ), value: string->toQStringNoThrow()); |
| 369 | dict.insert(QStringLiteral("haschild" ), value: false); |
| 370 | dict.insert(QStringLiteral("valueencoded" ), QStringLiteral("utf16" )); |
| 371 | dict.insert(QStringLiteral("quoted" ), value: true); |
| 372 | } else if (const QV4::ArrayObject *array = value.as<QV4::ArrayObject>()) { |
| 373 | // The size of an array is number of its numerical properties. |
| 374 | // We don't consider free form object properties here. |
| 375 | const uint n = array->getLength(); |
| 376 | dict.insert(QStringLiteral("value" ), value: qint64(n)); |
| 377 | dict.insert(QStringLiteral("valueencoded" ), QStringLiteral("itemcount" )); |
| 378 | dict.insert(QStringLiteral("haschild" ), value: qint64(n)); |
| 379 | if (isExpanded(iname)) { |
| 380 | QJsonArray children; |
| 381 | for (uint i = 0; i < n; ++i) { |
| 382 | QV4::ReturnedValue v = array->get(idx: i); |
| 383 | QV4::ScopedValue sval(scope, v); |
| 384 | collect(out: &children, parentIName: iname, name: QString::number(i), value: *sval); |
| 385 | } |
| 386 | dict.insert(QStringLiteral("children" ), value: children); |
| 387 | } |
| 388 | } else if (const QV4::Object *object = value.as<QV4::Object>()) { |
| 389 | QJsonArray children; |
| 390 | bool expanded = isExpanded(iname); |
| 391 | qint64 numProperties = 0; |
| 392 | QV4::ObjectIterator it(scope, object, QV4::ObjectIterator::EnumerableOnly); |
| 393 | QV4::ScopedProperty p(scope); |
| 394 | QV4::ScopedPropertyKey name(scope); |
| 395 | while (true) { |
| 396 | QV4::PropertyAttributes attrs; |
| 397 | name = it.next(pd: p, attributes: &attrs); |
| 398 | if (!name->isValid()) |
| 399 | break; |
| 400 | if (name->isStringOrSymbol()) { |
| 401 | ++numProperties; |
| 402 | if (expanded) { |
| 403 | QV4::Value v = p.property->value; |
| 404 | collect(out: &children, parentIName: iname, name: name->toQString(), value: v); |
| 405 | } |
| 406 | } |
| 407 | } |
| 408 | dict.insert(QStringLiteral("value" ), value: numProperties); |
| 409 | dict.insert(QStringLiteral("valueencoded" ), QStringLiteral("itemcount" )); |
| 410 | dict.insert(QStringLiteral("haschild" ), value: numProperties > 0); |
| 411 | if (expanded) |
| 412 | dict.insert(QStringLiteral("children" ), value: children); |
| 413 | } |
| 414 | break; |
| 415 | case QV4::Value::Integer_Type: |
| 416 | dict.insert(QStringLiteral("value" ), value: value.integerValue()); |
| 417 | dict.insert(QStringLiteral("haschild" ), value: false); |
| 418 | break; |
| 419 | default: // double |
| 420 | dict.insert(QStringLiteral("value" ), value: value.doubleValue()); |
| 421 | dict.insert(QStringLiteral("haschild" ), value: false); |
| 422 | } |
| 423 | |
| 424 | out->append(value: dict); |
| 425 | } |
| 426 | |
| 427 | void NativeDebugger::handleVariables(QJsonObject *response, const QJsonObject &arguments) |
| 428 | { |
| 429 | TRACE_PROTOCOL("Build variables" ); |
| 430 | QV4::CppStackFrame *frame = nullptr; |
| 431 | decodeFrame(f: arguments.value(key: QLatin1String("context" )).toString(), frame: &frame); |
| 432 | if (!frame) { |
| 433 | setError(response, QStringLiteral("No stack frame passed" )); |
| 434 | return; |
| 435 | } |
| 436 | TRACE_PROTOCOL("Context: " << frame); |
| 437 | |
| 438 | QV4::ExecutionEngine *engine = frame->v4Function->internalClass->engine; |
| 439 | if (!engine) { |
| 440 | setError(response, QStringLiteral("No execution engine passed" )); |
| 441 | return; |
| 442 | } |
| 443 | TRACE_PROTOCOL("Engine: " << engine); |
| 444 | |
| 445 | Collector collector(engine); |
| 446 | const QJsonArray expanded = arguments.value(key: QLatin1String("expanded" )).toArray(); |
| 447 | for (const QJsonValue ex : expanded) |
| 448 | collector.m_expanded.append(t: ex.toString()); |
| 449 | TRACE_PROTOCOL("Expanded: " << collector.m_expanded); |
| 450 | |
| 451 | QJsonArray output; |
| 452 | QV4::Scope scope(engine); |
| 453 | |
| 454 | QV4::ScopedValue thisObject(scope, frame->thisObject()); |
| 455 | collector.collect(out: &output, parentIName: QString(), QStringLiteral("this" ), value: thisObject); |
| 456 | QV4::Scoped<QV4::CallContext> callContext(scope, frame->callContext()); |
| 457 | if (callContext) { |
| 458 | QV4::Heap::InternalClass *ic = callContext->internalClass(); |
| 459 | QV4::ScopedValue v(scope); |
| 460 | for (uint i = 0; i < ic->size; ++i) { |
| 461 | QV4::ScopedValue stringOrSymbol(scope, ic->keyAt(index: i)); |
| 462 | QV4::ScopedString propName(scope, stringOrSymbol->toString(e: scope.engine)); |
| 463 | v = callContext->getProperty(name: propName); |
| 464 | collector.collect(out: &output, parentIName: QString(), name: propName->toQString(), value: v); |
| 465 | } |
| 466 | } |
| 467 | |
| 468 | response->insert(QStringLiteral("variables" ), value: output); |
| 469 | } |
| 470 | |
| 471 | void NativeDebugger::handleExpressions(QJsonObject *response, const QJsonObject &arguments) |
| 472 | { |
| 473 | TRACE_PROTOCOL("Evaluate expressions" ); |
| 474 | QV4::CppStackFrame *frame = nullptr; |
| 475 | decodeFrame(f: arguments.value(key: QLatin1String("context" )).toString(), frame: &frame); |
| 476 | if (!frame) { |
| 477 | setError(response, QStringLiteral("No stack frame passed" )); |
| 478 | return; |
| 479 | } |
| 480 | TRACE_PROTOCOL("Context: " << executionContext); |
| 481 | |
| 482 | QV4::ExecutionEngine *engine = frame->v4Function->internalClass->engine; |
| 483 | if (!engine) { |
| 484 | setError(response, QStringLiteral("No execution engine passed" )); |
| 485 | return; |
| 486 | } |
| 487 | TRACE_PROTOCOL("Engines: " << engine << m_engine); |
| 488 | |
| 489 | Collector collector(engine); |
| 490 | const QJsonArray expanded = arguments.value(key: QLatin1String("expanded" )).toArray(); |
| 491 | for (const QJsonValue ex : expanded) |
| 492 | collector.m_expanded.append(t: ex.toString()); |
| 493 | TRACE_PROTOCOL("Expanded: " << collector.m_expanded); |
| 494 | |
| 495 | QJsonArray output; |
| 496 | QV4::Scope scope(engine); |
| 497 | |
| 498 | const QJsonArray expressions = arguments.value(key: QLatin1String("expressions" )).toArray(); |
| 499 | for (const QJsonValue expr : expressions) { |
| 500 | QString expression = expr.toObject().value(key: QLatin1String("expression" )).toString(); |
| 501 | QString name = expr.toObject().value(key: QLatin1String("name" )).toString(); |
| 502 | TRACE_PROTOCOL("Evaluate expression: " << expression); |
| 503 | m_runningJob = true; |
| 504 | |
| 505 | QV4::ScopedValue result(scope, evaluateExpression(expression)); |
| 506 | |
| 507 | m_runningJob = false; |
| 508 | if (result->isUndefined()) { |
| 509 | QJsonObject dict; |
| 510 | dict.insert(QStringLiteral("name" ), value: name); |
| 511 | dict.insert(QStringLiteral("valueencoded" ), QStringLiteral("undefined" )); |
| 512 | output.append(value: dict); |
| 513 | } else if (result.ptr && result.ptr->rawValue()) { |
| 514 | collector.collect(out: &output, parentIName: QString(), name, value: *result); |
| 515 | } else { |
| 516 | QJsonObject dict; |
| 517 | dict.insert(QStringLiteral("name" ), value: name); |
| 518 | dict.insert(QStringLiteral("valueencoded" ), QStringLiteral("notaccessible" )); |
| 519 | output.append(value: dict); |
| 520 | } |
| 521 | TRACE_PROTOCOL("EXCEPTION: " << engine->hasException); |
| 522 | engine->hasException = false; |
| 523 | } |
| 524 | |
| 525 | response->insert(QStringLiteral("expressions" ), value: output); |
| 526 | } |
| 527 | |
| 528 | void BreakPointHandler::removeBreakPoint(int id) |
| 529 | { |
| 530 | for (int i = 0; i != m_breakPoints.size(); ++i) { |
| 531 | if (m_breakPoints.at(i).id == id) { |
| 532 | m_breakPoints.remove(i); |
| 533 | m_haveBreakPoints = !m_breakPoints.isEmpty(); |
| 534 | return; |
| 535 | } |
| 536 | } |
| 537 | } |
| 538 | |
| 539 | void BreakPointHandler::enableBreakPoint(int id, bool enabled) |
| 540 | { |
| 541 | m_breakPoints[id].enabled = enabled; |
| 542 | } |
| 543 | |
| 544 | void NativeDebugger::pause() |
| 545 | { |
| 546 | m_pauseRequested = true; |
| 547 | } |
| 548 | |
| 549 | void NativeDebugger::handleContinue(QJsonObject *response, Speed speed) |
| 550 | { |
| 551 | Q_UNUSED(response); |
| 552 | |
| 553 | if (!m_returnedValue.isUndefined()) |
| 554 | m_returnedValue.set(engine: m_engine, value: QV4::Encode::undefined()); |
| 555 | |
| 556 | m_currentFrame = m_engine->currentStackFrame; |
| 557 | m_stepping = speed; |
| 558 | } |
| 559 | |
| 560 | void NativeDebugger::maybeBreakAtInstruction() |
| 561 | { |
| 562 | if (m_runningJob) // do not re-enter when we're doing a job for the debugger. |
| 563 | return; |
| 564 | |
| 565 | if (m_stepping == StepOver) { |
| 566 | if (m_currentFrame == m_engine->currentStackFrame) |
| 567 | pauseAndWait(); |
| 568 | return; |
| 569 | } |
| 570 | |
| 571 | if (m_stepping == StepIn) { |
| 572 | pauseAndWait(); |
| 573 | return; |
| 574 | } |
| 575 | |
| 576 | if (m_pauseRequested) { // Serve debugging requests from the agent |
| 577 | m_pauseRequested = false; |
| 578 | pauseAndWait(); |
| 579 | return; |
| 580 | } |
| 581 | |
| 582 | if (m_service->m_breakHandler->m_haveBreakPoints) { |
| 583 | if (QV4::Function *function = getFunction()) { |
| 584 | // lineNumber will be negative for Ret instructions, so those won't match |
| 585 | const int lineNumber = m_engine->currentStackFrame->lineNumber(); |
| 586 | if (reallyHitTheBreakPoint(function, lineNumber)) |
| 587 | pauseAndWait(); |
| 588 | } |
| 589 | } |
| 590 | } |
| 591 | |
| 592 | void NativeDebugger::enteringFunction() |
| 593 | { |
| 594 | if (m_runningJob) |
| 595 | return; |
| 596 | |
| 597 | if (m_stepping == StepIn) { |
| 598 | m_currentFrame = m_engine->currentStackFrame; |
| 599 | } |
| 600 | } |
| 601 | |
| 602 | void NativeDebugger::leavingFunction(const QV4::ReturnedValue &retVal) |
| 603 | { |
| 604 | if (m_runningJob) |
| 605 | return; |
| 606 | |
| 607 | if (m_stepping != NotStepping && m_currentFrame == m_engine->currentStackFrame) { |
| 608 | m_currentFrame = m_currentFrame->parentFrame(); |
| 609 | m_stepping = StepOver; |
| 610 | m_returnedValue.set(engine: m_engine, value: retVal); |
| 611 | } |
| 612 | } |
| 613 | |
| 614 | void NativeDebugger::aboutToThrow() |
| 615 | { |
| 616 | if (!m_service->m_breakHandler->m_breakOnThrow) |
| 617 | return; |
| 618 | |
| 619 | if (m_runningJob) // do not re-enter when we're doing a job for the debugger. |
| 620 | return; |
| 621 | |
| 622 | QJsonObject event; |
| 623 | // TODO: complete this! |
| 624 | event.insert(QStringLiteral("event" ), QStringLiteral("exception" )); |
| 625 | m_service->emitAsynchronousMessageToClient(message: event); |
| 626 | } |
| 627 | |
| 628 | QV4::Function *NativeDebugger::getFunction() const |
| 629 | { |
| 630 | if (m_engine->currentStackFrame) |
| 631 | return m_engine->currentStackFrame->v4Function; |
| 632 | else |
| 633 | return m_engine->globalCode; |
| 634 | } |
| 635 | |
| 636 | void NativeDebugger::pauseAndWait() |
| 637 | { |
| 638 | QJsonObject event; |
| 639 | |
| 640 | event.insert(QStringLiteral("event" ), QStringLiteral("break" )); |
| 641 | event.insert(QStringLiteral("language" ), QStringLiteral("js" )); |
| 642 | if (QV4::CppStackFrame *frame = m_engine->currentStackFrame) { |
| 643 | QV4::Function *function = frame->v4Function; |
| 644 | event.insert(QStringLiteral("file" ), value: function->sourceFile()); |
| 645 | int line = frame->lineNumber(); |
| 646 | event.insert(QStringLiteral("line" ), value: (line < 0 ? -line : line)); |
| 647 | } |
| 648 | |
| 649 | m_service->emitAsynchronousMessageToClient(message: event); |
| 650 | } |
| 651 | |
| 652 | bool NativeDebugger::reallyHitTheBreakPoint(const QV4::Function *function, int lineNumber) |
| 653 | { |
| 654 | for (int i = 0, n = m_service->m_breakHandler->m_breakPoints.size(); i != n; ++i) { |
| 655 | const BreakPoint &bp = m_service->m_breakHandler->m_breakPoints.at(i); |
| 656 | if (bp.lineNumber == lineNumber) { |
| 657 | const QString base = QUrl(function->sourceFile()).fileName(); |
| 658 | if (bp.fileName.endsWith(s: base)) { |
| 659 | if (bp.condition.isEmpty() || checkCondition(expression: bp.condition)) { |
| 660 | BreakPoint &mbp = m_service->m_breakHandler->m_breakPoints[i]; |
| 661 | ++mbp.hitCount; |
| 662 | if (mbp.hitCount > mbp.ignoreCount) |
| 663 | return true; |
| 664 | } |
| 665 | } |
| 666 | } |
| 667 | } |
| 668 | return false; |
| 669 | } |
| 670 | |
| 671 | QQmlNativeDebugServiceImpl::QQmlNativeDebugServiceImpl(QObject *parent) |
| 672 | : QQmlNativeDebugService(1.0, parent) |
| 673 | { |
| 674 | m_breakHandler = new BreakPointHandler; |
| 675 | } |
| 676 | |
| 677 | QQmlNativeDebugServiceImpl::~QQmlNativeDebugServiceImpl() |
| 678 | { |
| 679 | delete m_breakHandler; |
| 680 | } |
| 681 | |
| 682 | void QQmlNativeDebugServiceImpl::engineAboutToBeAdded(QJSEngine *engine) |
| 683 | { |
| 684 | TRACE_PROTOCOL("Adding engine" << engine); |
| 685 | if (engine) { |
| 686 | QV4::ExecutionEngine *ee = engine->handle(); |
| 687 | TRACE_PROTOCOL("Adding execution engine" << ee); |
| 688 | if (ee) { |
| 689 | NativeDebugger *debugger = new NativeDebugger(this, ee); |
| 690 | if (state() == Enabled) |
| 691 | ee->setDebugger(debugger); |
| 692 | m_debuggers.append(t: QPointer<NativeDebugger>(debugger)); |
| 693 | } |
| 694 | } |
| 695 | QQmlDebugService::engineAboutToBeAdded(engine); |
| 696 | } |
| 697 | |
| 698 | void QQmlNativeDebugServiceImpl::engineAboutToBeRemoved(QJSEngine *engine) |
| 699 | { |
| 700 | TRACE_PROTOCOL("Removing engine" << engine); |
| 701 | if (engine) { |
| 702 | QV4::ExecutionEngine *executionEngine = engine->handle(); |
| 703 | const auto debuggersCopy = m_debuggers; |
| 704 | for (NativeDebugger *debugger : debuggersCopy) { |
| 705 | if (debugger->engine() == executionEngine) |
| 706 | m_debuggers.removeAll(t: debugger); |
| 707 | } |
| 708 | } |
| 709 | QQmlDebugService::engineAboutToBeRemoved(engine); |
| 710 | } |
| 711 | |
| 712 | void QQmlNativeDebugServiceImpl::stateAboutToBeChanged(QQmlDebugService::State state) |
| 713 | { |
| 714 | if (state == Enabled) { |
| 715 | for (NativeDebugger *debugger : std::as_const(t&: m_debuggers)) { |
| 716 | QV4::ExecutionEngine *engine = debugger->engine(); |
| 717 | if (!engine->debugger()) |
| 718 | engine->setDebugger(debugger); |
| 719 | } |
| 720 | } |
| 721 | QQmlDebugService::stateAboutToBeChanged(state); |
| 722 | } |
| 723 | |
| 724 | void QQmlNativeDebugServiceImpl::messageReceived(const QByteArray &message) |
| 725 | { |
| 726 | TRACE_PROTOCOL("Native message received: " << message); |
| 727 | QJsonObject request = QJsonDocument::fromJson(json: message).object(); |
| 728 | QJsonObject response; |
| 729 | QJsonObject arguments = request.value(key: QLatin1String("arguments" )).toObject(); |
| 730 | QString cmd = request.value(key: QLatin1String("command" )).toString(); |
| 731 | |
| 732 | if (cmd == QLatin1String("setbreakpoint" )) { |
| 733 | m_breakHandler->handleSetBreakpoint(response: &response, arguments); |
| 734 | } else if (cmd == QLatin1String("removebreakpoint" )) { |
| 735 | m_breakHandler->handleRemoveBreakpoint(response: &response, arguments); |
| 736 | } else if (cmd == QLatin1String("echo" )) { |
| 737 | response.insert(QStringLiteral("result" ), value: arguments); |
| 738 | } else { |
| 739 | for (NativeDebugger *debugger : std::as_const(t&: m_debuggers)) |
| 740 | if (debugger) |
| 741 | debugger->handleCommand(response: &response, cmd, arguments); |
| 742 | } |
| 743 | QJsonDocument doc; |
| 744 | doc.setObject(response); |
| 745 | QByteArray ba = doc.toJson(format: QJsonDocument::Compact); |
| 746 | TRACE_PROTOCOL("Sending synchronous response:" << ba.constData() << endl); |
| 747 | emit messageToClient(name: s_key, message: ba); |
| 748 | } |
| 749 | |
| 750 | void QQmlNativeDebugServiceImpl::emitAsynchronousMessageToClient(const QJsonObject &message) |
| 751 | { |
| 752 | QJsonDocument doc; |
| 753 | doc.setObject(message); |
| 754 | QByteArray ba = doc.toJson(format: QJsonDocument::Compact); |
| 755 | TRACE_PROTOCOL("Sending asynchronous message:" << ba.constData() << endl); |
| 756 | emit messageToClient(name: s_key, message: ba); |
| 757 | } |
| 758 | |
| 759 | QT_END_NAMESPACE |
| 760 | |