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 "qqmldebugserverfactory.h" |
5 | |
6 | #include <private/qqmldebugserver_p.h> |
7 | #include <private/qqmldebugserverconnection_p.h> |
8 | #include <private/qqmldebugservice_p.h> |
9 | #include <private/qjsengine_p.h> |
10 | #include <private/qqmlglobal_p.h> |
11 | #include <private/qqmldebugpluginmanager_p.h> |
12 | #include <private/qqmldebugserviceinterfaces_p.h> |
13 | #include <private/qpacketprotocol_p.h> |
14 | #include <private/qversionedpacket_p.h> |
15 | |
16 | #include <QtCore/QAtomicInt> |
17 | #include <QtCore/QDir> |
18 | #include <QtCore/QPluginLoader> |
19 | #include <QtCore/QStringList> |
20 | #include <QtCore/QVector> |
21 | #include <QtCore/qwaitcondition.h> |
22 | |
23 | QT_BEGIN_NAMESPACE |
24 | |
25 | /* |
26 | QQmlDebug Protocol (Version 1): |
27 | |
28 | handshake: |
29 | 1. Client sends |
30 | "QDeclarativeDebugServer" 0 version pluginNames [QDataStream version] |
31 | version: an int representing the highest protocol version the client knows |
32 | pluginNames: plugins available on client side |
33 | 2. Server sends |
34 | "QDeclarativeDebugClient" 0 version pluginNames pluginVersions [QDataStream version] |
35 | version: an int representing the highest protocol version the client & server know |
36 | pluginNames: plugins available on server side. plugins both in the client and server message are enabled. |
37 | client plugin advertisement |
38 | 1. Client sends |
39 | "QDeclarativeDebugServer" 1 pluginNames |
40 | server plugin advertisement (not implemented: all services are required to register before open()) |
41 | 1. Server sends |
42 | "QDeclarativeDebugClient" 1 pluginNames pluginVersions |
43 | plugin communication: |
44 | Everything send with a header different to "QDeclarativeDebugServer" is sent to the appropriate plugin. |
45 | */ |
46 | |
47 | Q_QML_DEBUG_PLUGIN_LOADER(QQmlDebugServerConnection) |
48 | |
49 | const int protocolVersion = 1; |
50 | using QQmlDebugPacket = QVersionedPacket<QQmlDebugConnector>; |
51 | |
52 | class QQmlDebugServerImpl; |
53 | class QQmlDebugServerThread : public QThread |
54 | { |
55 | public: |
56 | QQmlDebugServerThread() : m_server(nullptr), m_portFrom(-1), m_portTo(-1) {} |
57 | |
58 | void setServer(QQmlDebugServerImpl *server) |
59 | { |
60 | m_server = server; |
61 | } |
62 | |
63 | void setPortRange(int portFrom, int portTo, const QString &hostAddress) |
64 | { |
65 | m_pluginName = QLatin1String("QTcpServerConnection" ); |
66 | m_portFrom = portFrom; |
67 | m_portTo = portTo; |
68 | m_hostAddress = hostAddress; |
69 | } |
70 | |
71 | void setFileName(const QString &fileName) |
72 | { |
73 | m_pluginName = QLatin1String("QLocalClientConnection" ); |
74 | m_fileName = fileName; |
75 | } |
76 | |
77 | const QString &pluginName() const |
78 | { |
79 | return m_pluginName; |
80 | } |
81 | |
82 | void run() override; |
83 | |
84 | private: |
85 | QQmlDebugServerImpl *m_server; |
86 | QString m_pluginName; |
87 | int m_portFrom; |
88 | int m_portTo; |
89 | QString m_hostAddress; |
90 | QString m_fileName; |
91 | }; |
92 | |
93 | class QQmlDebugServerImpl : public QQmlDebugServer |
94 | { |
95 | Q_OBJECT |
96 | public: |
97 | QQmlDebugServerImpl(); |
98 | |
99 | bool blockingMode() const override; |
100 | |
101 | QQmlDebugService *service(const QString &name) const override; |
102 | |
103 | void addEngine(QJSEngine *engine) override; |
104 | void removeEngine(QJSEngine *engine) override; |
105 | bool hasEngine(QJSEngine *engine) const override; |
106 | |
107 | bool addService(const QString &name, QQmlDebugService *service) override; |
108 | bool removeService(const QString &name) override; |
109 | |
110 | bool open(const QVariantHash &configuration) override; |
111 | void setDevice(QIODevice *socket) override; |
112 | |
113 | void parseArguments(); |
114 | |
115 | static void cleanup(); |
116 | |
117 | private: |
118 | friend class QQmlDebugServerThread; |
119 | friend class QQmlDebugServerFactory; |
120 | |
121 | class EngineCondition { |
122 | public: |
123 | EngineCondition() : numServices(0), condition(new QWaitCondition) {} |
124 | |
125 | bool waitForServices(QMutex *locked, int numEngines); |
126 | bool isWaiting() const { return numServices > 0; } |
127 | |
128 | void wake(); |
129 | private: |
130 | int numServices; |
131 | |
132 | // shared pointer to allow for QHash-inflicted copying. |
133 | QSharedPointer<QWaitCondition> condition; |
134 | }; |
135 | |
136 | bool canSendMessage(const QString &name); |
137 | void doSendMessage(const QString &name, const QByteArray &message); |
138 | void wakeEngine(QJSEngine *engine); |
139 | void sendMessage(const QString &name, const QByteArray &message); |
140 | void sendMessages(const QString &name, const QList<QByteArray> &messages); |
141 | void changeServiceState(const QString &serviceName, QQmlDebugService::State state); |
142 | void removeThread(); |
143 | void receiveMessage(); |
144 | void protocolError(); |
145 | |
146 | QQmlDebugServerConnection *m_connection; |
147 | QHash<QString, QQmlDebugService *> m_plugins; |
148 | QStringList m_clientPlugins; |
149 | bool m_gotHello; |
150 | bool m_blockingMode; |
151 | |
152 | QHash<QJSEngine *, EngineCondition> m_engineConditions; |
153 | |
154 | mutable QMutex m_helloMutex; |
155 | QWaitCondition m_helloCondition; |
156 | QQmlDebugServerThread m_thread; |
157 | QPacketProtocol *m_protocol; |
158 | QAtomicInt m_changeServiceStateCalls; |
159 | }; |
160 | |
161 | void QQmlDebugServerImpl::cleanup() |
162 | { |
163 | QQmlDebugServerImpl *server = static_cast<QQmlDebugServerImpl *>( |
164 | QQmlDebugConnector::instance()); |
165 | if (!server) |
166 | return; |
167 | |
168 | { |
169 | QObject signalSource; |
170 | for (QHash<QString, QQmlDebugService *>::ConstIterator i = server->m_plugins.constBegin(); |
171 | i != server->m_plugins.constEnd(); ++i) { |
172 | server->m_changeServiceStateCalls.ref(); |
173 | QString key = i.key(); |
174 | // Process this in the server's thread. |
175 | connect(sender: &signalSource, signal: &QObject::destroyed, context: server, slot: [key, server](){ |
176 | server->changeServiceState(serviceName: key, state: QQmlDebugService::NotConnected); |
177 | }, type: Qt::QueuedConnection); |
178 | } |
179 | } |
180 | |
181 | // Wait for changeServiceState calls to finish |
182 | // (while running an event loop because some services |
183 | // might again defer execution of stuff in the GUI thread) |
184 | QEventLoop loop; |
185 | while (!server->m_changeServiceStateCalls.testAndSetOrdered(expectedValue: 0, newValue: 0)) |
186 | loop.processEvents(); |
187 | |
188 | // Stop the thread while the application is still there. |
189 | server->m_thread.exit(); |
190 | server->m_thread.wait(); |
191 | } |
192 | |
193 | void QQmlDebugServerThread::run() |
194 | { |
195 | Q_ASSERT_X(m_server != nullptr, Q_FUNC_INFO, "There should always be a debug server available here." ); |
196 | QQmlDebugServerConnection *connection = loadQQmlDebugServerConnection(key: m_pluginName); |
197 | if (connection) { |
198 | { |
199 | QMutexLocker connectionLocker(&m_server->m_helloMutex); |
200 | m_server->m_connection = connection; |
201 | connection->setServer(m_server); |
202 | m_server->m_helloCondition.wakeAll(); |
203 | } |
204 | |
205 | if (m_fileName.isEmpty()) { |
206 | if (!connection->setPortRange(portFrom: m_portFrom, portTo: m_portTo, block: m_server->blockingMode(), |
207 | hostaddress: m_hostAddress)) |
208 | return; |
209 | } else if (!connection->setFileName(fileName: m_fileName, block: m_server->blockingMode())) { |
210 | return; |
211 | } |
212 | |
213 | if (m_server->blockingMode()) |
214 | connection->waitForConnection(); |
215 | } else { |
216 | qWarning() << "QML Debugger: Couldn't load plugin" << m_pluginName; |
217 | return; |
218 | } |
219 | |
220 | exec(); |
221 | |
222 | // make sure events still waiting are processed |
223 | QEventLoop eventLoop; |
224 | eventLoop.processEvents(flags: QEventLoop::AllEvents); |
225 | } |
226 | |
227 | bool QQmlDebugServerImpl::blockingMode() const |
228 | { |
229 | return m_blockingMode; |
230 | } |
231 | |
232 | static void cleanupOnShutdown() |
233 | { |
234 | // We cannot do this in the destructor as the connection plugin will get unloaded before the |
235 | // server plugin and we need the connection to send any remaining data. This function is |
236 | // triggered before any plugins are unloaded. |
237 | QQmlDebugServerImpl::cleanup(); |
238 | } |
239 | |
240 | QQmlDebugServerImpl::QQmlDebugServerImpl() : |
241 | m_connection(nullptr), |
242 | m_gotHello(false), |
243 | m_blockingMode(false) |
244 | { |
245 | static bool postRoutineAdded = false; |
246 | if (!postRoutineAdded) { |
247 | qAddPostRoutine(cleanupOnShutdown); |
248 | postRoutineAdded = true; |
249 | } |
250 | |
251 | // used in sendMessages |
252 | qRegisterMetaType<QList<QByteArray> >(typeName: "QList<QByteArray>" ); |
253 | // used in changeServiceState |
254 | qRegisterMetaType<QQmlDebugService::State>(typeName: "QQmlDebugService::State" ); |
255 | |
256 | m_thread.setServer(this); |
257 | moveToThread(thread: &m_thread); |
258 | |
259 | // Remove the thread immmediately when it finishes, so that we don't have to wait for the |
260 | // event loop to signal that. |
261 | QObject::connect(sender: &m_thread, signal: &QThread::finished, context: this, slot: &QQmlDebugServerImpl::removeThread, |
262 | type: Qt::DirectConnection); |
263 | m_thread.setObjectName(QStringLiteral("QQmlDebugServerThread" )); |
264 | parseArguments(); |
265 | } |
266 | |
267 | bool QQmlDebugServerImpl::open(const QVariantHash &configuration = QVariantHash()) |
268 | { |
269 | if (m_thread.isRunning()) |
270 | return false; |
271 | if (!configuration.isEmpty()) { |
272 | m_blockingMode = configuration[QLatin1String("block" )].toBool(); |
273 | if (configuration.contains(key: QLatin1String("portFrom" ))) { |
274 | int portFrom = configuration[QLatin1String("portFrom" )].toInt(); |
275 | int portTo = configuration[QLatin1String("portTo" )].toInt(); |
276 | m_thread.setPortRange(portFrom, portTo: portTo == -1 ? portFrom : portTo, |
277 | hostAddress: configuration[QLatin1String("hostAddress" )].toString()); |
278 | } else if (configuration.contains(key: QLatin1String("fileName" ))) { |
279 | m_thread.setFileName(configuration[QLatin1String("fileName" )].toString()); |
280 | } else { |
281 | return false; |
282 | } |
283 | } |
284 | |
285 | if (m_thread.pluginName().isEmpty()) |
286 | return false; |
287 | |
288 | QMutexLocker locker(&m_helloMutex); |
289 | m_thread.start(); |
290 | m_helloCondition.wait(lockedMutex: &m_helloMutex); // wait for connection |
291 | if (m_blockingMode && !m_gotHello) |
292 | m_helloCondition.wait(lockedMutex: &m_helloMutex); // wait for hello |
293 | return true; |
294 | } |
295 | |
296 | void QQmlDebugServerImpl::parseArguments() |
297 | { |
298 | // format: qmljsdebugger=port:<port_from>[,port_to],host:<ip address>][,block] |
299 | const QString args = commandLineArguments(); |
300 | if (args.isEmpty()) |
301 | return; // Manual initialization, through QQmlDebugServer::open() |
302 | |
303 | // ### remove port definition when protocol is changed |
304 | int portFrom = 0; |
305 | int portTo = 0; |
306 | bool block = false; |
307 | bool ok = false; |
308 | QString hostAddress; |
309 | QString fileName; |
310 | QStringList services; |
311 | |
312 | const auto lstjsDebugArguments = QStringView{args}.split(sep: QLatin1Char(','), behavior: Qt::SkipEmptyParts); |
313 | for (auto argsIt = lstjsDebugArguments.begin(), argsItEnd = lstjsDebugArguments.end(); argsIt != argsItEnd; ++argsIt) { |
314 | const QStringView &strArgument = *argsIt; |
315 | if (strArgument.startsWith(s: QLatin1String("port:" ))) { |
316 | portFrom = strArgument.mid(pos: 5).toInt(ok: &ok); |
317 | portTo = portFrom; |
318 | const auto argsNext = argsIt + 1; |
319 | if (argsNext == argsItEnd) |
320 | break; |
321 | if (ok) { |
322 | portTo = argsNext->toString().toInt(ok: &ok); |
323 | if (ok) { |
324 | ++argsIt; |
325 | } else { |
326 | portTo = portFrom; |
327 | ok = true; |
328 | } |
329 | } |
330 | } else if (strArgument.startsWith(s: QLatin1String("host:" ))) { |
331 | hostAddress = strArgument.mid(pos: 5).toString(); |
332 | } else if (strArgument == QLatin1String("block" )) { |
333 | block = true; |
334 | } else if (strArgument.startsWith(s: QLatin1String("file:" ))) { |
335 | fileName = strArgument.mid(pos: 5).toString(); |
336 | ok = !fileName.isEmpty(); |
337 | } else if (strArgument.startsWith(s: QLatin1String("services:" ))) { |
338 | services.append(t: strArgument.mid(pos: 9).toString()); |
339 | } else if (!services.isEmpty()) { |
340 | services.append(t: strArgument.toString()); |
341 | } else if (!strArgument.startsWith(s: QLatin1String("connector:" ))) { |
342 | const QString message = tr(s: "QML Debugger: Invalid argument \"%1\" detected." |
343 | " Ignoring the same." ).arg(a: strArgument.toString()); |
344 | qWarning(msg: "%s" , qPrintable(message)); |
345 | } |
346 | } |
347 | |
348 | if (ok) { |
349 | setServices(services); |
350 | m_blockingMode = block; |
351 | if (!fileName.isEmpty()) |
352 | m_thread.setFileName(fileName); |
353 | else |
354 | m_thread.setPortRange(portFrom, portTo, hostAddress); |
355 | } else { |
356 | QString usage; |
357 | QTextStream str(&usage); |
358 | str << tr(s: "QML Debugger: Ignoring \"-qmljsdebugger=%1\"." ).arg(a: args) << '\n' |
359 | << tr(s: "The format is \"-qmljsdebugger=[file:<file>|port:<port_from>][,<port_to>]" |
360 | "[,host:<ip address>][,block][,services:<service>][,<service>]*\"" ) << '\n' |
361 | << tr(s: "\"file:\" can be used to specify the name of a file the debugger will try " |
362 | "to connect to using a QLocalSocket. If \"file:\" is given any \"host:\" and" |
363 | "\"port:\" arguments will be ignored." ) << '\n' |
364 | << tr(s: "\"host:\" and \"port:\" can be used to specify an address and a single " |
365 | "port or a range of ports the debugger will try to bind to with a " |
366 | "QTcpServer." ) << '\n' |
367 | << tr(s: "\"block\" makes the debugger and some services wait for clients to be " |
368 | "connected and ready before the first QML engine starts." ) << '\n' |
369 | << tr(s: "\"services:\" can be used to specify which debug services the debugger " |
370 | "should load. Some debug services interact badly with others. The V4 " |
371 | "debugger should not be loaded when using the QML profiler as it will force " |
372 | "any V4 engines to use the JavaScript interpreter rather than the JIT. The " |
373 | "following debug services are available by default:" ) << '\n' |
374 | << QQmlEngineDebugService::s_key << "\t- " << tr(s: "The QML debugger" ) << '\n' |
375 | << QV4DebugService::s_key << "\t- " << tr(s: "The V4 debugger" ) << '\n' |
376 | << QQmlInspectorService::s_key << "\t- " << tr(s: "The QML inspector" ) << '\n' |
377 | << QQmlProfilerService::s_key << "\t- " << tr(s: "The QML profiler" ) << '\n' |
378 | << QQmlEngineControlService::s_key << "\t- " |
379 | //: Please preserve the line breaks and formatting |
380 | << tr(s: "Allows the client to delay the starting and stopping of\n" |
381 | "\t\t QML engines until other services are ready. QtCreator\n" |
382 | "\t\t uses this service with the QML profiler in order to\n" |
383 | "\t\t profile multiple QML engines at the same time." ) |
384 | << '\n' << QDebugMessageService::s_key << "\t- " |
385 | //: Please preserve the line breaks and formatting |
386 | << tr(s: "Sends qDebug() and similar messages over the QML debug\n" |
387 | "\t\t connection. QtCreator uses this for showing debug\n" |
388 | "\t\t messages in the debugger console." ) << '\n' |
389 | #if QT_CONFIG(translation) |
390 | << '\n' << QQmlDebugTranslationService::s_key << "\t- " |
391 | //: Please preserve the line breaks and formatting |
392 | << tr(s: "helps to see if a translated text\n" |
393 | "\t\t will result in an elided text\n" |
394 | "\t\t in QML elements." ) << '\n' |
395 | #endif //QT_CONFIG(translation) |
396 | << tr(s: "Other services offered by qmltooling plugins that implement " |
397 | "QQmlDebugServiceFactory and which can be found in the standard plugin " |
398 | "paths will also be available and can be specified. If no \"services\" " |
399 | "argument is given, all services found this way, including the default " |
400 | "ones, are loaded." ); |
401 | qWarning(msg: "%s" , qPrintable(usage)); |
402 | } |
403 | } |
404 | |
405 | void QQmlDebugServerImpl::receiveMessage() |
406 | { |
407 | typedef QHash<QString, QQmlDebugService*>::const_iterator DebugServiceConstIt; |
408 | |
409 | // to be executed in debugger thread |
410 | Q_ASSERT(QThread::currentThread() == thread()); |
411 | |
412 | if (!m_protocol) |
413 | return; |
414 | |
415 | QQmlDebugPacket in(m_protocol->read()); |
416 | |
417 | QString name; |
418 | |
419 | in >> name; |
420 | if (name == QLatin1String("QDeclarativeDebugServer" )) { |
421 | int op = -1; |
422 | in >> op; |
423 | if (op == 0) { |
424 | int version; |
425 | in >> version >> m_clientPlugins; |
426 | |
427 | //Get the supported QDataStream version |
428 | if (!in.atEnd()) { |
429 | in >> s_dataStreamVersion; |
430 | if (s_dataStreamVersion > QDataStream::Qt_DefaultCompiledVersion) |
431 | s_dataStreamVersion = QDataStream::Qt_DefaultCompiledVersion; |
432 | } |
433 | |
434 | bool clientSupportsMultiPackets = false; |
435 | if (!in.atEnd()) |
436 | in >> clientSupportsMultiPackets; |
437 | |
438 | // Send the hello answer immediately, since it needs to arrive before |
439 | // the plugins below start sending messages. |
440 | |
441 | QQmlDebugPacket out; |
442 | QStringList pluginNames; |
443 | QList<float> pluginVersions; |
444 | if (clientSupportsMultiPackets) { // otherwise, disable all plugins |
445 | const int count = m_plugins.size(); |
446 | pluginNames.reserve(asize: count); |
447 | pluginVersions.reserve(asize: count); |
448 | for (QHash<QString, QQmlDebugService *>::ConstIterator i = m_plugins.constBegin(); |
449 | i != m_plugins.constEnd(); ++i) { |
450 | pluginNames << i.key(); |
451 | pluginVersions << i.value()->version(); |
452 | } |
453 | } |
454 | |
455 | out << QString(QStringLiteral("QDeclarativeDebugClient" )) << 0 << protocolVersion |
456 | << pluginNames << pluginVersions << dataStreamVersion(); |
457 | |
458 | m_protocol->send(data: out.data()); |
459 | m_connection->flush(); |
460 | |
461 | QMutexLocker helloLock(&m_helloMutex); |
462 | m_gotHello = true; |
463 | |
464 | for (DebugServiceConstIt iter = m_plugins.constBegin(), cend = m_plugins.constEnd(); iter != cend; ++iter) { |
465 | QQmlDebugService::State newState = QQmlDebugService::Unavailable; |
466 | if (m_clientPlugins.contains(str: iter.key())) |
467 | newState = QQmlDebugService::Enabled; |
468 | m_changeServiceStateCalls.ref(); |
469 | changeServiceState(serviceName: iter.key(), state: newState); |
470 | } |
471 | |
472 | m_helloCondition.wakeAll(); |
473 | |
474 | } else if (op == 1) { |
475 | // Service Discovery |
476 | QStringList oldClientPlugins = m_clientPlugins; |
477 | in >> m_clientPlugins; |
478 | |
479 | for (DebugServiceConstIt iter = m_plugins.constBegin(), cend = m_plugins.constEnd(); iter != cend; ++iter) { |
480 | const QString &pluginName = iter.key(); |
481 | QQmlDebugService::State newState = QQmlDebugService::Unavailable; |
482 | if (m_clientPlugins.contains(str: pluginName)) |
483 | newState = QQmlDebugService::Enabled; |
484 | |
485 | if (oldClientPlugins.contains(str: pluginName) |
486 | != m_clientPlugins.contains(str: pluginName)) { |
487 | m_changeServiceStateCalls.ref(); |
488 | changeServiceState(serviceName: iter.key(), state: newState); |
489 | } |
490 | } |
491 | |
492 | } else { |
493 | qWarning(msg: "QML Debugger: Invalid control message %d." , op); |
494 | protocolError(); |
495 | return; |
496 | } |
497 | |
498 | } else { |
499 | if (m_gotHello) { |
500 | QHash<QString, QQmlDebugService *>::Iterator iter = m_plugins.find(key: name); |
501 | if (iter == m_plugins.end()) { |
502 | qWarning() << "QML Debugger: Message received for missing plugin" << name << '.'; |
503 | } else { |
504 | QQmlDebugService *service = *iter; |
505 | QByteArray message; |
506 | while (!in.atEnd()) { |
507 | in >> message; |
508 | service->messageReceived(message); |
509 | } |
510 | } |
511 | } else { |
512 | qWarning(msg: "QML Debugger: Invalid hello message." ); |
513 | } |
514 | |
515 | } |
516 | } |
517 | |
518 | void QQmlDebugServerImpl::changeServiceState(const QString &serviceName, |
519 | QQmlDebugService::State newState) |
520 | { |
521 | // to be executed in debugger thread |
522 | Q_ASSERT(QThread::currentThread() == thread()); |
523 | |
524 | QQmlDebugService *service = m_plugins.value(key: serviceName); |
525 | if (service && service->state() != newState) { |
526 | service->stateAboutToBeChanged(newState); |
527 | service->setState(newState); |
528 | service->stateChanged(newState); |
529 | } |
530 | |
531 | m_changeServiceStateCalls.deref(); |
532 | } |
533 | |
534 | void QQmlDebugServerImpl::removeThread() |
535 | { |
536 | Q_ASSERT(m_thread.isFinished()); |
537 | Q_ASSERT(QThread::currentThread() == thread()); |
538 | |
539 | QThread *parentThread = m_thread.thread(); |
540 | |
541 | delete m_connection; |
542 | m_connection = nullptr; |
543 | |
544 | // Move it back to the parent thread so that we can potentially restart it on a new thread. |
545 | moveToThread(thread: parentThread); |
546 | } |
547 | |
548 | QQmlDebugService *QQmlDebugServerImpl::service(const QString &name) const |
549 | { |
550 | return m_plugins.value(key: name); |
551 | } |
552 | |
553 | void QQmlDebugServerImpl::addEngine(QJSEngine *engine) |
554 | { |
555 | // to be executed outside of debugger thread |
556 | Q_ASSERT(QThread::currentThread() != &m_thread); |
557 | |
558 | QMutexLocker locker(&m_helloMutex); |
559 | Q_ASSERT(!m_engineConditions.contains(engine)); |
560 | |
561 | for (QQmlDebugService *service : std::as_const(t&: m_plugins)) |
562 | service->engineAboutToBeAdded(engine); |
563 | |
564 | m_engineConditions[engine].waitForServices(locked: &m_helloMutex, numEngines: m_plugins.size()); |
565 | |
566 | for (QQmlDebugService *service : std::as_const(t&: m_plugins)) |
567 | service->engineAdded(engine); |
568 | } |
569 | |
570 | void QQmlDebugServerImpl::removeEngine(QJSEngine *engine) |
571 | { |
572 | // to be executed outside of debugger thread |
573 | Q_ASSERT(QThread::currentThread() != &m_thread); |
574 | |
575 | QMutexLocker locker(&m_helloMutex); |
576 | Q_ASSERT(m_engineConditions.contains(engine)); |
577 | |
578 | for (QQmlDebugService *service : std::as_const(t&: m_plugins)) |
579 | service->engineAboutToBeRemoved(engine); |
580 | |
581 | m_engineConditions[engine].waitForServices(locked: &m_helloMutex, numEngines: m_plugins.size()); |
582 | |
583 | for (QQmlDebugService *service : std::as_const(t&: m_plugins)) |
584 | service->engineRemoved(engine); |
585 | |
586 | m_engineConditions.remove(key: engine); |
587 | } |
588 | |
589 | bool QQmlDebugServerImpl::hasEngine(QJSEngine *engine) const |
590 | { |
591 | QMutexLocker locker(&m_helloMutex); |
592 | QHash<QJSEngine *, EngineCondition>::ConstIterator i = m_engineConditions.constFind(key: engine); |
593 | // if we're still waiting the engine isn't fully "there", yet, nor fully removed. |
594 | return i != m_engineConditions.constEnd() && !i.value().isWaiting(); |
595 | } |
596 | |
597 | bool QQmlDebugServerImpl::addService(const QString &name, QQmlDebugService *service) |
598 | { |
599 | // to be executed before thread starts |
600 | Q_ASSERT(!m_thread.isRunning()); |
601 | |
602 | if (!service || m_plugins.contains(key: name)) |
603 | return false; |
604 | |
605 | connect(sender: service, signal: &QQmlDebugService::messageToClient, |
606 | context: this, slot: &QQmlDebugServerImpl::sendMessage); |
607 | connect(sender: service, signal: &QQmlDebugService::messagesToClient, |
608 | context: this, slot: &QQmlDebugServerImpl::sendMessages); |
609 | |
610 | connect(sender: service, signal: &QQmlDebugService::attachedToEngine, |
611 | context: this, slot: &QQmlDebugServerImpl::wakeEngine, type: Qt::QueuedConnection); |
612 | connect(sender: service, signal: &QQmlDebugService::detachedFromEngine, |
613 | context: this, slot: &QQmlDebugServerImpl::wakeEngine, type: Qt::QueuedConnection); |
614 | |
615 | service->setState(QQmlDebugService::Unavailable); |
616 | m_plugins.insert(key: name, value: service); |
617 | |
618 | return true; |
619 | } |
620 | |
621 | bool QQmlDebugServerImpl::removeService(const QString &name) |
622 | { |
623 | // to be executed after thread ends |
624 | Q_ASSERT(!m_thread.isRunning()); |
625 | |
626 | QQmlDebugService *service = m_plugins.value(key: name); |
627 | if (!service) |
628 | return false; |
629 | |
630 | m_plugins.remove(key: name); |
631 | service->setState(QQmlDebugService::NotConnected); |
632 | |
633 | disconnect(sender: service, signal: &QQmlDebugService::detachedFromEngine, |
634 | receiver: this, slot: &QQmlDebugServerImpl::wakeEngine); |
635 | disconnect(sender: service, signal: &QQmlDebugService::attachedToEngine, |
636 | receiver: this, slot: &QQmlDebugServerImpl::wakeEngine); |
637 | |
638 | disconnect(sender: service, signal: &QQmlDebugService::messagesToClient, |
639 | receiver: this, slot: &QQmlDebugServerImpl::sendMessages); |
640 | disconnect(sender: service, signal: &QQmlDebugService::messageToClient, |
641 | receiver: this, slot: &QQmlDebugServerImpl::sendMessage); |
642 | |
643 | return true; |
644 | } |
645 | |
646 | bool QQmlDebugServerImpl::canSendMessage(const QString &name) |
647 | { |
648 | // to be executed in debugger thread |
649 | Q_ASSERT(QThread::currentThread() == thread()); |
650 | return m_connection && m_connection->isConnected() && m_protocol && |
651 | m_clientPlugins.contains(str: name); |
652 | } |
653 | |
654 | void QQmlDebugServerImpl::doSendMessage(const QString &name, const QByteArray &message) |
655 | { |
656 | QQmlDebugPacket out; |
657 | out << name << message; |
658 | m_protocol->send(data: out.data()); |
659 | } |
660 | |
661 | void QQmlDebugServerImpl::sendMessage(const QString &name, const QByteArray &message) |
662 | { |
663 | if (canSendMessage(name)) { |
664 | doSendMessage(name, message); |
665 | m_connection->flush(); |
666 | } |
667 | } |
668 | |
669 | void QQmlDebugServerImpl::sendMessages(const QString &name, const QList<QByteArray> &messages) |
670 | { |
671 | if (canSendMessage(name)) { |
672 | QQmlDebugPacket out; |
673 | out << name; |
674 | for (const QByteArray &message : messages) |
675 | out << message; |
676 | m_protocol->send(data: out.data()); |
677 | m_connection->flush(); |
678 | } |
679 | } |
680 | |
681 | void QQmlDebugServerImpl::wakeEngine(QJSEngine *engine) |
682 | { |
683 | // to be executed in debugger thread |
684 | Q_ASSERT(QThread::currentThread() == thread()); |
685 | |
686 | QMutexLocker locker(&m_helloMutex); |
687 | m_engineConditions[engine].wake(); |
688 | } |
689 | |
690 | bool QQmlDebugServerImpl::EngineCondition::waitForServices(QMutex *locked, int num) |
691 | { |
692 | Q_ASSERT_X(numServices == 0, Q_FUNC_INFO, "Request to wait again before previous wait finished" ); |
693 | numServices = num; |
694 | return numServices > 0 ? condition->wait(lockedMutex: locked) : true; |
695 | } |
696 | |
697 | void QQmlDebugServerImpl::EngineCondition::wake() |
698 | { |
699 | if (--numServices == 0) |
700 | condition->wakeAll(); |
701 | Q_ASSERT_X(numServices >=0, Q_FUNC_INFO, "Woken more often than #services." ); |
702 | } |
703 | |
704 | void QQmlDebugServerImpl::setDevice(QIODevice *socket) |
705 | { |
706 | m_protocol = new QPacketProtocol(socket, this); |
707 | QObject::connect(sender: m_protocol, signal: &QPacketProtocol::readyRead, |
708 | context: this, slot: &QQmlDebugServerImpl::receiveMessage); |
709 | QObject::connect(sender: m_protocol, signal: &QPacketProtocol::error, |
710 | context: this, slot: &QQmlDebugServerImpl::protocolError); |
711 | |
712 | if (blockingMode()) |
713 | m_protocol->waitForReadyRead(msecs: -1); |
714 | } |
715 | |
716 | void QQmlDebugServerImpl::protocolError() |
717 | { |
718 | qWarning(msg: "QML Debugger: A protocol error has occurred! Giving up ..." ); |
719 | m_connection->disconnect(); |
720 | // protocol might still be processing packages at this point |
721 | m_protocol->deleteLater(); |
722 | m_protocol = nullptr; |
723 | } |
724 | |
725 | QQmlDebugConnector *QQmlDebugServerFactory::create(const QString &key) |
726 | { |
727 | // Cannot parent it to this because it gets moved to another thread |
728 | return (key == QLatin1String("QQmlDebugServer" ) ? new QQmlDebugServerImpl : nullptr); |
729 | } |
730 | |
731 | QT_END_NAMESPACE |
732 | |
733 | #include "moc_qqmldebugserverfactory.cpp" |
734 | #include "qqmldebugserverfactory.moc" |
735 | |