// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only

#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 = QStringView{args}.split(QLatin1Char(','), Qt::SkipEmptyParts);
    for (auto argsIt = lstjsDebugArguments.begin(), argsItEnd = lstjsDebugArguments.end(); argsIt != argsItEnd; ++argsIt) {
        const QStringView &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'
#if QT_CONFIG(translation)
            << '\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'
#endif //QT_CONFIG(translation)
           << 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.size();
                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) {
            const auto iter = m_plugins.constFind(name);
            if (iter == m_plugins.cend()) {
                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 : std::as_const(m_plugins))
        service->engineAboutToBeAdded(engine);

    m_engineConditions[engine].waitForServices(&m_helloMutex, m_plugins.size());

    for (QQmlDebugService *service : std::as_const(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 : std::as_const(m_plugins))
        service->engineAboutToBeRemoved(engine);

    m_engineConditions[engine].waitForServices(&m_helloMutex, m_plugins.size());

    for (QQmlDebugService *service : std::as_const(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 "moc_qqmldebugserverfactory.cpp"
#include "qqmldebugserverfactory.moc"
