| /**************************************************************************** |
| ** |
| ** 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 "qqmldebugserverfactory.h" |
| |
| #include <private/qqmldebugserver_p.h> |
| #include <private/qqmldebugserverconnection_p.h> |
| #include <private/qqmldebugservice_p.h> |
| #include <private/qjsengine_p.h> |
| #include <private/qqmlglobal_p.h> |
| #include <private/qqmldebugpluginmanager_p.h> |
| #include <private/qqmldebugserviceinterfaces_p.h> |
| #include <private/qpacketprotocol_p.h> |
| #include <private/qversionedpacket_p.h> |
| |
| #include <QtCore/QAtomicInt> |
| #include <QtCore/QDir> |
| #include <QtCore/QPluginLoader> |
| #include <QtCore/QStringList> |
| #include <QtCore/QVector> |
| #include <QtCore/qwaitcondition.h> |
| |
| QT_BEGIN_NAMESPACE |
| |
| /* |
| QQmlDebug Protocol (Version 1): |
| |
| handshake: |
| 1. Client sends |
| "QDeclarativeDebugServer" 0 version pluginNames [QDataStream version] |
| version: an int representing the highest protocol version the client knows |
| pluginNames: plugins available on client side |
| 2. Server sends |
| "QDeclarativeDebugClient" 0 version pluginNames pluginVersions [QDataStream version] |
| version: an int representing the highest protocol version the client & server know |
| pluginNames: plugins available on server side. plugins both in the client and server message are enabled. |
| client plugin advertisement |
| 1. Client sends |
| "QDeclarativeDebugServer" 1 pluginNames |
| server plugin advertisement (not implemented: all services are required to register before open()) |
| 1. Server sends |
| "QDeclarativeDebugClient" 1 pluginNames pluginVersions |
| plugin communication: |
| Everything send with a header different to "QDeclarativeDebugServer" is sent to the appropriate plugin. |
| */ |
| |
| Q_QML_DEBUG_PLUGIN_LOADER(QQmlDebugServerConnection) |
| |
| const int protocolVersion = 1; |
| using QQmlDebugPacket = QVersionedPacket<QQmlDebugConnector>; |
| |
| class QQmlDebugServerImpl; |
| class QQmlDebugServerThread : public QThread |
| { |
| public: |
| QQmlDebugServerThread() : m_server(nullptr), m_portFrom(-1), m_portTo(-1) {} |
| |
| void setServer(QQmlDebugServerImpl *server) |
| { |
| m_server = server; |
| } |
| |
| void setPortRange(int portFrom, int portTo, const QString &hostAddress) |
| { |
| m_pluginName = QLatin1String("QTcpServerConnection"); |
| m_portFrom = portFrom; |
| m_portTo = portTo; |
| m_hostAddress = hostAddress; |
| } |
| |
| void setFileName(const QString &fileName) |
| { |
| m_pluginName = QLatin1String("QLocalClientConnection"); |
| m_fileName = fileName; |
| } |
| |
| const QString &pluginName() const |
| { |
| return m_pluginName; |
| } |
| |
| void run() override; |
| |
| private: |
| QQmlDebugServerImpl *m_server; |
| QString m_pluginName; |
| int m_portFrom; |
| int m_portTo; |
| QString m_hostAddress; |
| QString m_fileName; |
| }; |
| |
| class QQmlDebugServerImpl : public QQmlDebugServer |
| { |
| Q_OBJECT |
| public: |
| QQmlDebugServerImpl(); |
| |
| bool blockingMode() const override; |
| |
| QQmlDebugService *service(const QString &name) const override; |
| |
| void addEngine(QJSEngine *engine) override; |
| void removeEngine(QJSEngine *engine) override; |
| bool hasEngine(QJSEngine *engine) const override; |
| |
| bool addService(const QString &name, QQmlDebugService *service) override; |
| bool removeService(const QString &name) override; |
| |
| bool open(const QVariantHash &configuration) override; |
| void setDevice(QIODevice *socket) override; |
| |
| void parseArguments(); |
| |
| static void cleanup(); |
| |
| private: |
| friend class QQmlDebugServerThread; |
| friend class QQmlDebugServerFactory; |
| |
| class EngineCondition { |
| public: |
| EngineCondition() : numServices(0), condition(new QWaitCondition) {} |
| |
| bool waitForServices(QMutex *locked, int numEngines); |
| bool isWaiting() const { return numServices > 0; } |
| |
| void wake(); |
| private: |
| int numServices; |
| |
| // shared pointer to allow for QHash-inflicted copying. |
| QSharedPointer<QWaitCondition> condition; |
| }; |
| |
| bool canSendMessage(const QString &name); |
| void doSendMessage(const QString &name, const QByteArray &message); |
| void wakeEngine(QJSEngine *engine); |
| void sendMessage(const QString &name, const QByteArray &message); |
| void sendMessages(const QString &name, const QList<QByteArray> &messages); |
| void changeServiceState(const QString &serviceName, QQmlDebugService::State state); |
| void removeThread(); |
| void receiveMessage(); |
| void protocolError(); |
| |
| QQmlDebugServerConnection *m_connection; |
| QHash<QString, QQmlDebugService *> m_plugins; |
| QStringList m_clientPlugins; |
| bool m_gotHello; |
| bool m_blockingMode; |
| |
| QHash<QJSEngine *, EngineCondition> m_engineConditions; |
| |
| mutable QMutex m_helloMutex; |
| QWaitCondition m_helloCondition; |
| QQmlDebugServerThread m_thread; |
| QPacketProtocol *m_protocol; |
| QAtomicInt m_changeServiceStateCalls; |
| }; |
| |
| void QQmlDebugServerImpl::cleanup() |
| { |
| QQmlDebugServerImpl *server = static_cast<QQmlDebugServerImpl *>( |
| QQmlDebugConnector::instance()); |
| if (!server) |
| return; |
| |
| { |
| QObject signalSource; |
| for (QHash<QString, QQmlDebugService *>::ConstIterator i = server->m_plugins.constBegin(); |
| i != server->m_plugins.constEnd(); ++i) { |
| server->m_changeServiceStateCalls.ref(); |
| QString key = i.key(); |
| // Process this in the server's thread. |
| connect(&signalSource, &QObject::destroyed, server, [key, server](){ |
| server->changeServiceState(key, QQmlDebugService::NotConnected); |
| }, Qt::QueuedConnection); |
| } |
| } |
| |
| // Wait for changeServiceState calls to finish |
| // (while running an event loop because some services |
| // might again defer execution of stuff in the GUI thread) |
| QEventLoop loop; |
| while (!server->m_changeServiceStateCalls.testAndSetOrdered(0, 0)) |
| loop.processEvents(); |
| |
| // Stop the thread while the application is still there. |
| server->m_thread.exit(); |
| server->m_thread.wait(); |
| } |
| |
| void QQmlDebugServerThread::run() |
| { |
| Q_ASSERT_X(m_server != nullptr, Q_FUNC_INFO, "There should always be a debug server available here."); |
| QQmlDebugServerConnection *connection = loadQQmlDebugServerConnection(m_pluginName); |
| if (connection) { |
| { |
| QMutexLocker connectionLocker(&m_server->m_helloMutex); |
| m_server->m_connection = connection; |
| connection->setServer(m_server); |
| m_server->m_helloCondition.wakeAll(); |
| } |
| |
| if (m_fileName.isEmpty()) { |
| if (!connection->setPortRange(m_portFrom, m_portTo, m_server->blockingMode(), |
| m_hostAddress)) |
| return; |
| } else if (!connection->setFileName(m_fileName, m_server->blockingMode())) { |
| return; |
| } |
| |
| if (m_server->blockingMode()) |
| connection->waitForConnection(); |
| } else { |
| qWarning() << "QML Debugger: Couldn't load plugin" << m_pluginName; |
| return; |
| } |
| |
| exec(); |
| |
| // make sure events still waiting are processed |
| QEventLoop eventLoop; |
| eventLoop.processEvents(QEventLoop::AllEvents); |
| } |
| |
| bool QQmlDebugServerImpl::blockingMode() const |
| { |
| return m_blockingMode; |
| } |
| |
| static void cleanupOnShutdown() |
| { |
| // We cannot do this in the destructor as the connection plugin will get unloaded before the |
| // server plugin and we need the connection to send any remaining data. This function is |
| // triggered before any plugins are unloaded. |
| QQmlDebugServerImpl::cleanup(); |
| } |
| |
| QQmlDebugServerImpl::QQmlDebugServerImpl() : |
| m_connection(nullptr), |
| m_gotHello(false), |
| m_blockingMode(false) |
| { |
| static bool postRoutineAdded = false; |
| if (!postRoutineAdded) { |
| qAddPostRoutine(cleanupOnShutdown); |
| postRoutineAdded = true; |
| } |
| |
| // used in sendMessages |
| qRegisterMetaType<QList<QByteArray> >("QList<QByteArray>"); |
| // used in changeServiceState |
| qRegisterMetaType<QQmlDebugService::State>("QQmlDebugService::State"); |
| |
| m_thread.setServer(this); |
| moveToThread(&m_thread); |
| |
| // Remove the thread immmediately when it finishes, so that we don't have to wait for the |
| // event loop to signal that. |
| QObject::connect(&m_thread, &QThread::finished, this, &QQmlDebugServerImpl::removeThread, |
| Qt::DirectConnection); |
| m_thread.setObjectName(QStringLiteral("QQmlDebugServerThread")); |
| parseArguments(); |
| } |
| |
| bool QQmlDebugServerImpl::open(const QVariantHash &configuration = QVariantHash()) |
| { |
| if (m_thread.isRunning()) |
| return false; |
| if (!configuration.isEmpty()) { |
| m_blockingMode = configuration[QLatin1String("block")].toBool(); |
| if (configuration.contains(QLatin1String("portFrom"))) { |
| int portFrom = configuration[QLatin1String("portFrom")].toInt(); |
| int portTo = configuration[QLatin1String("portTo")].toInt(); |
| m_thread.setPortRange(portFrom, portTo == -1 ? portFrom : portTo, |
| configuration[QLatin1String("hostAddress")].toString()); |
| } else if (configuration.contains(QLatin1String("fileName"))) { |
| m_thread.setFileName(configuration[QLatin1String("fileName")].toString()); |
| } else { |
| return false; |
| } |
| } |
| |
| if (m_thread.pluginName().isEmpty()) |
| return false; |
| |
| QMutexLocker locker(&m_helloMutex); |
| m_thread.start(); |
| m_helloCondition.wait(&m_helloMutex); // wait for connection |
| if (m_blockingMode && !m_gotHello) |
| m_helloCondition.wait(&m_helloMutex); // wait for hello |
| return true; |
| } |
| |
| void QQmlDebugServerImpl::parseArguments() |
| { |
| // format: qmljsdebugger=port:<port_from>[,port_to],host:<ip address>][,block] |
| const QString args = commandLineArguments(); |
| if (args.isEmpty()) |
| return; // Manual initialization, through QQmlDebugServer::open() |
| |
| // ### remove port definition when protocol is changed |
| int portFrom = 0; |
| int portTo = 0; |
| bool block = false; |
| bool ok = false; |
| QString hostAddress; |
| QString fileName; |
| QStringList services; |
| |
| const auto lstjsDebugArguments = args.splitRef(QLatin1Char(','), Qt::SkipEmptyParts); |
| for (auto argsIt = lstjsDebugArguments.begin(), argsItEnd = lstjsDebugArguments.end(); argsIt != argsItEnd; ++argsIt) { |
| const QStringRef &strArgument = *argsIt; |
| if (strArgument.startsWith(QLatin1String("port:"))) { |
| portFrom = strArgument.mid(5).toInt(&ok); |
| portTo = portFrom; |
| const auto argsNext = argsIt + 1; |
| if (argsNext == argsItEnd) |
| break; |
| if (ok) { |
| portTo = argsNext->toString().toInt(&ok); |
| if (ok) { |
| ++argsIt; |
| } else { |
| portTo = portFrom; |
| ok = true; |
| } |
| } |
| } else if (strArgument.startsWith(QLatin1String("host:"))) { |
| hostAddress = strArgument.mid(5).toString(); |
| } else if (strArgument == QLatin1String("block")) { |
| block = true; |
| } else if (strArgument.startsWith(QLatin1String("file:"))) { |
| fileName = strArgument.mid(5).toString(); |
| ok = !fileName.isEmpty(); |
| } else if (strArgument.startsWith(QLatin1String("services:"))) { |
| services.append(strArgument.mid(9).toString()); |
| } else if (!services.isEmpty()) { |
| services.append(strArgument.toString()); |
| } else if (!strArgument.startsWith(QLatin1String("connector:"))) { |
| const QString message = tr("QML Debugger: Invalid argument \"%1\" detected." |
| " Ignoring the same.").arg(strArgument.toString()); |
| qWarning("%s", qPrintable(message)); |
| } |
| } |
| |
| if (ok) { |
| setServices(services); |
| m_blockingMode = block; |
| if (!fileName.isEmpty()) |
| m_thread.setFileName(fileName); |
| else |
| m_thread.setPortRange(portFrom, portTo, hostAddress); |
| } else { |
| QString usage; |
| QTextStream str(&usage); |
| str << tr("QML Debugger: Ignoring \"-qmljsdebugger=%1\".").arg(args) << '\n' |
| << tr("The format is \"-qmljsdebugger=[file:<file>|port:<port_from>][,<port_to>]" |
| "[,host:<ip address>][,block][,services:<service>][,<service>]*\"") << '\n' |
| << tr("\"file:\" can be used to specify the name of a file the debugger will try " |
| "to connect to using a QLocalSocket. If \"file:\" is given any \"host:\" and" |
| "\"port:\" arguments will be ignored.") << '\n' |
| << tr("\"host:\" and \"port:\" can be used to specify an address and a single " |
| "port or a range of ports the debugger will try to bind to with a " |
| "QTcpServer.") << '\n' |
| << tr("\"block\" makes the debugger and some services wait for clients to be " |
| "connected and ready before the first QML engine starts.") << '\n' |
| << tr("\"services:\" can be used to specify which debug services the debugger " |
| "should load. Some debug services interact badly with others. The V4 " |
| "debugger should not be loaded when using the QML profiler as it will force " |
| "any V4 engines to use the JavaScript interpreter rather than the JIT. The " |
| "following debug services are available by default:") << '\n' |
| << QQmlEngineDebugService::s_key << "\t- " << tr("The QML debugger") << '\n' |
| << QV4DebugService::s_key << "\t- " << tr("The V4 debugger") << '\n' |
| << QQmlInspectorService::s_key << "\t- " << tr("The QML inspector") << '\n' |
| << QQmlProfilerService::s_key << "\t- " << tr("The QML profiler") << '\n' |
| << QQmlEngineControlService::s_key << "\t- " |
| //: Please preserve the line breaks and formatting |
| << tr("Allows the client to delay the starting and stopping of\n" |
| "\t\t QML engines until other services are ready. QtCreator\n" |
| "\t\t uses this service with the QML profiler in order to\n" |
| "\t\t profile multiple QML engines at the same time.") |
| << '\n' << QDebugMessageService::s_key << "\t- " |
| //: Please preserve the line breaks and formatting |
| << tr("Sends qDebug() and similar messages over the QML debug\n" |
| "\t\t connection. QtCreator uses this for showing debug\n" |
| "\t\t messages in the debugger console.") << '\n' |
| << '\n' << QQmlDebugTranslationService::s_key << "\t- " |
| //: Please preserve the line breaks and formatting |
| << tr("helps to see if a translated text\n" |
| "\t\t will result in an elided text\n" |
| "\t\t in QML elements.") << '\n' |
| << tr("Other services offered by qmltooling plugins that implement " |
| "QQmlDebugServiceFactory and which can be found in the standard plugin " |
| "paths will also be available and can be specified. If no \"services\" " |
| "argument is given, all services found this way, including the default " |
| "ones, are loaded."); |
| qWarning("%s", qPrintable(usage)); |
| } |
| } |
| |
| void QQmlDebugServerImpl::receiveMessage() |
| { |
| typedef QHash<QString, QQmlDebugService*>::const_iterator DebugServiceConstIt; |
| |
| // to be executed in debugger thread |
| Q_ASSERT(QThread::currentThread() == thread()); |
| |
| if (!m_protocol) |
| return; |
| |
| QQmlDebugPacket in(m_protocol->read()); |
| |
| QString name; |
| |
| in >> name; |
| if (name == QLatin1String("QDeclarativeDebugServer")) { |
| int op = -1; |
| in >> op; |
| if (op == 0) { |
| int version; |
| in >> version >> m_clientPlugins; |
| |
| //Get the supported QDataStream version |
| if (!in.atEnd()) { |
| in >> s_dataStreamVersion; |
| if (s_dataStreamVersion > QDataStream::Qt_DefaultCompiledVersion) |
| s_dataStreamVersion = QDataStream::Qt_DefaultCompiledVersion; |
| } |
| |
| bool clientSupportsMultiPackets = false; |
| if (!in.atEnd()) |
| in >> clientSupportsMultiPackets; |
| |
| // Send the hello answer immediately, since it needs to arrive before |
| // the plugins below start sending messages. |
| |
| QQmlDebugPacket out; |
| QStringList pluginNames; |
| QList<float> pluginVersions; |
| if (clientSupportsMultiPackets) { // otherwise, disable all plugins |
| const int count = m_plugins.count(); |
| pluginNames.reserve(count); |
| pluginVersions.reserve(count); |
| for (QHash<QString, QQmlDebugService *>::ConstIterator i = m_plugins.constBegin(); |
| i != m_plugins.constEnd(); ++i) { |
| pluginNames << i.key(); |
| pluginVersions << i.value()->version(); |
| } |
| } |
| |
| out << QString(QStringLiteral("QDeclarativeDebugClient")) << 0 << protocolVersion |
| << pluginNames << pluginVersions << dataStreamVersion(); |
| |
| m_protocol->send(out.data()); |
| m_connection->flush(); |
| |
| QMutexLocker helloLock(&m_helloMutex); |
| m_gotHello = true; |
| |
| for (DebugServiceConstIt iter = m_plugins.constBegin(), cend = m_plugins.constEnd(); iter != cend; ++iter) { |
| QQmlDebugService::State newState = QQmlDebugService::Unavailable; |
| if (m_clientPlugins.contains(iter.key())) |
| newState = QQmlDebugService::Enabled; |
| m_changeServiceStateCalls.ref(); |
| changeServiceState(iter.key(), newState); |
| } |
| |
| m_helloCondition.wakeAll(); |
| |
| } else if (op == 1) { |
| // Service Discovery |
| QStringList oldClientPlugins = m_clientPlugins; |
| in >> m_clientPlugins; |
| |
| for (DebugServiceConstIt iter = m_plugins.constBegin(), cend = m_plugins.constEnd(); iter != cend; ++iter) { |
| const QString &pluginName = iter.key(); |
| QQmlDebugService::State newState = QQmlDebugService::Unavailable; |
| if (m_clientPlugins.contains(pluginName)) |
| newState = QQmlDebugService::Enabled; |
| |
| if (oldClientPlugins.contains(pluginName) |
| != m_clientPlugins.contains(pluginName)) { |
| m_changeServiceStateCalls.ref(); |
| changeServiceState(iter.key(), newState); |
| } |
| } |
| |
| } else { |
| qWarning("QML Debugger: Invalid control message %d.", op); |
| protocolError(); |
| return; |
| } |
| |
| } else { |
| if (m_gotHello) { |
| QHash<QString, QQmlDebugService *>::Iterator iter = m_plugins.find(name); |
| if (iter == m_plugins.end()) { |
| qWarning() << "QML Debugger: Message received for missing plugin" << name << '.'; |
| } else { |
| QQmlDebugService *service = *iter; |
| QByteArray message; |
| while (!in.atEnd()) { |
| in >> message; |
| service->messageReceived(message); |
| } |
| } |
| } else { |
| qWarning("QML Debugger: Invalid hello message."); |
| } |
| |
| } |
| } |
| |
| void QQmlDebugServerImpl::changeServiceState(const QString &serviceName, |
| QQmlDebugService::State newState) |
| { |
| // to be executed in debugger thread |
| Q_ASSERT(QThread::currentThread() == thread()); |
| |
| QQmlDebugService *service = m_plugins.value(serviceName); |
| if (service && service->state() != newState) { |
| service->stateAboutToBeChanged(newState); |
| service->setState(newState); |
| service->stateChanged(newState); |
| } |
| |
| m_changeServiceStateCalls.deref(); |
| } |
| |
| void QQmlDebugServerImpl::removeThread() |
| { |
| Q_ASSERT(m_thread.isFinished()); |
| Q_ASSERT(QThread::currentThread() == thread()); |
| |
| QThread *parentThread = m_thread.thread(); |
| |
| delete m_connection; |
| m_connection = nullptr; |
| |
| // Move it back to the parent thread so that we can potentially restart it on a new thread. |
| moveToThread(parentThread); |
| } |
| |
| QQmlDebugService *QQmlDebugServerImpl::service(const QString &name) const |
| { |
| return m_plugins.value(name); |
| } |
| |
| void QQmlDebugServerImpl::addEngine(QJSEngine *engine) |
| { |
| // to be executed outside of debugger thread |
| Q_ASSERT(QThread::currentThread() != &m_thread); |
| |
| QMutexLocker locker(&m_helloMutex); |
| Q_ASSERT(!m_engineConditions.contains(engine)); |
| |
| for (QQmlDebugService *service : qAsConst(m_plugins)) |
| service->engineAboutToBeAdded(engine); |
| |
| m_engineConditions[engine].waitForServices(&m_helloMutex, m_plugins.count()); |
| |
| for (QQmlDebugService *service : qAsConst(m_plugins)) |
| service->engineAdded(engine); |
| } |
| |
| void QQmlDebugServerImpl::removeEngine(QJSEngine *engine) |
| { |
| // to be executed outside of debugger thread |
| Q_ASSERT(QThread::currentThread() != &m_thread); |
| |
| QMutexLocker locker(&m_helloMutex); |
| Q_ASSERT(m_engineConditions.contains(engine)); |
| |
| for (QQmlDebugService *service : qAsConst(m_plugins)) |
| service->engineAboutToBeRemoved(engine); |
| |
| m_engineConditions[engine].waitForServices(&m_helloMutex, m_plugins.count()); |
| |
| for (QQmlDebugService *service : qAsConst(m_plugins)) |
| service->engineRemoved(engine); |
| |
| m_engineConditions.remove(engine); |
| } |
| |
| bool QQmlDebugServerImpl::hasEngine(QJSEngine *engine) const |
| { |
| QMutexLocker locker(&m_helloMutex); |
| QHash<QJSEngine *, EngineCondition>::ConstIterator i = m_engineConditions.constFind(engine); |
| // if we're still waiting the engine isn't fully "there", yet, nor fully removed. |
| return i != m_engineConditions.constEnd() && !i.value().isWaiting(); |
| } |
| |
| bool QQmlDebugServerImpl::addService(const QString &name, QQmlDebugService *service) |
| { |
| // to be executed before thread starts |
| Q_ASSERT(!m_thread.isRunning()); |
| |
| if (!service || m_plugins.contains(name)) |
| return false; |
| |
| connect(service, &QQmlDebugService::messageToClient, |
| this, &QQmlDebugServerImpl::sendMessage); |
| connect(service, &QQmlDebugService::messagesToClient, |
| this, &QQmlDebugServerImpl::sendMessages); |
| |
| connect(service, &QQmlDebugService::attachedToEngine, |
| this, &QQmlDebugServerImpl::wakeEngine, Qt::QueuedConnection); |
| connect(service, &QQmlDebugService::detachedFromEngine, |
| this, &QQmlDebugServerImpl::wakeEngine, Qt::QueuedConnection); |
| |
| service->setState(QQmlDebugService::Unavailable); |
| m_plugins.insert(name, service); |
| |
| return true; |
| } |
| |
| bool QQmlDebugServerImpl::removeService(const QString &name) |
| { |
| // to be executed after thread ends |
| Q_ASSERT(!m_thread.isRunning()); |
| |
| QQmlDebugService *service = m_plugins.value(name); |
| if (!service) |
| return false; |
| |
| m_plugins.remove(name); |
| service->setState(QQmlDebugService::NotConnected); |
| |
| disconnect(service, &QQmlDebugService::detachedFromEngine, |
| this, &QQmlDebugServerImpl::wakeEngine); |
| disconnect(service, &QQmlDebugService::attachedToEngine, |
| this, &QQmlDebugServerImpl::wakeEngine); |
| |
| disconnect(service, &QQmlDebugService::messagesToClient, |
| this, &QQmlDebugServerImpl::sendMessages); |
| disconnect(service, &QQmlDebugService::messageToClient, |
| this, &QQmlDebugServerImpl::sendMessage); |
| |
| return true; |
| } |
| |
| bool QQmlDebugServerImpl::canSendMessage(const QString &name) |
| { |
| // to be executed in debugger thread |
| Q_ASSERT(QThread::currentThread() == thread()); |
| return m_connection && m_connection->isConnected() && m_protocol && |
| m_clientPlugins.contains(name); |
| } |
| |
| void QQmlDebugServerImpl::doSendMessage(const QString &name, const QByteArray &message) |
| { |
| QQmlDebugPacket out; |
| out << name << message; |
| m_protocol->send(out.data()); |
| } |
| |
| void QQmlDebugServerImpl::sendMessage(const QString &name, const QByteArray &message) |
| { |
| if (canSendMessage(name)) { |
| doSendMessage(name, message); |
| m_connection->flush(); |
| } |
| } |
| |
| void QQmlDebugServerImpl::sendMessages(const QString &name, const QList<QByteArray> &messages) |
| { |
| if (canSendMessage(name)) { |
| QQmlDebugPacket out; |
| out << name; |
| for (const QByteArray &message : messages) |
| out << message; |
| m_protocol->send(out.data()); |
| m_connection->flush(); |
| } |
| } |
| |
| void QQmlDebugServerImpl::wakeEngine(QJSEngine *engine) |
| { |
| // to be executed in debugger thread |
| Q_ASSERT(QThread::currentThread() == thread()); |
| |
| QMutexLocker locker(&m_helloMutex); |
| m_engineConditions[engine].wake(); |
| } |
| |
| bool QQmlDebugServerImpl::EngineCondition::waitForServices(QMutex *locked, int num) |
| { |
| Q_ASSERT_X(numServices == 0, Q_FUNC_INFO, "Request to wait again before previous wait finished"); |
| numServices = num; |
| return numServices > 0 ? condition->wait(locked) : true; |
| } |
| |
| void QQmlDebugServerImpl::EngineCondition::wake() |
| { |
| if (--numServices == 0) |
| condition->wakeAll(); |
| Q_ASSERT_X(numServices >=0, Q_FUNC_INFO, "Woken more often than #services."); |
| } |
| |
| void QQmlDebugServerImpl::setDevice(QIODevice *socket) |
| { |
| m_protocol = new QPacketProtocol(socket, this); |
| QObject::connect(m_protocol, &QPacketProtocol::readyRead, |
| this, &QQmlDebugServerImpl::receiveMessage); |
| QObject::connect(m_protocol, &QPacketProtocol::error, |
| this, &QQmlDebugServerImpl::protocolError); |
| |
| if (blockingMode()) |
| m_protocol->waitForReadyRead(-1); |
| } |
| |
| void QQmlDebugServerImpl::protocolError() |
| { |
| qWarning("QML Debugger: A protocol error has occurred! Giving up ..."); |
| m_connection->disconnect(); |
| // protocol might still be processing packages at this point |
| m_protocol->deleteLater(); |
| m_protocol = nullptr; |
| } |
| |
| QQmlDebugConnector *QQmlDebugServerFactory::create(const QString &key) |
| { |
| // Cannot parent it to this because it gets moved to another thread |
| return (key == QLatin1String("QQmlDebugServer") ? new QQmlDebugServerImpl : nullptr); |
| } |
| |
| QT_END_NAMESPACE |
| |
| #include "qqmldebugserver.moc" |