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