From fb587a3a996d2e9d2fca34cc053df29f6d23f64e Mon Sep 17 00:00:00 2001 From: Milian Wolff Date: Wed, 11 Dec 2013 15:54:33 +0100 Subject: Restructure sources and assimilate to Qt module structure. This module can hopefully be done in time for 5.3. This commit changes the source structure and QMake files to adapt to typical Qt modules. With this in place, we can now use QT += webchannel in qmake files to link against the pure Qt/C++ QtWebChannel library. The QML plugin is separated from it and can be loaded optionally, if the quick module could be found. Also added is now a qmlplugindump for tooling integration. Note that the Qt.labs namespace is removed. The test file structure is also adapted to how its done in the QtDeclarative module. Note that this setup apparently does not support to run tests without running make install first. Change-Id: I1c15d72e7ab5f525d5a6f651f4e965ef86bc17bd Reviewed-by: Simon Hausmann --- src/imports/imports.pro | 3 + src/imports/webchannel/plugin.cpp | 69 +++ src/imports/webchannel/plugins.qmltypes | 98 +++++ src/imports/webchannel/qmldir | 3 + src/imports/webchannel/webchannel.pro | 6 + src/plugin.json | 3 - src/qmldir | 2 - src/qobject.js | 307 -------------- src/qtmetaobjectpublisher.cpp | 731 -------------------------------- src/qtmetaobjectpublisher.h | 114 ----- src/qwebchannel.cpp | 209 --------- src/qwebchannel.h | 91 ---- src/qwebchannel_plugin.cpp | 54 --- src/qwebchannel_plugin.h | 56 --- src/qwebsocketserver.cpp | 422 ------------------ src/qwebsocketserver.h | 158 ------- src/resources.qrc | 6 - src/signalhandler.h | 278 ------------ src/src.pri | 11 - src/src.pro | 42 +- src/variantargument.h | 103 ----- src/webchannel.js | 131 ------ src/webchannel/qmetaobjectpublisher.cpp | 730 +++++++++++++++++++++++++++++++ src/webchannel/qmetaobjectpublisher.h | 114 +++++ src/webchannel/qobject.js | 305 +++++++++++++ src/webchannel/qwebchannel.cpp | 207 +++++++++ src/webchannel/qwebchannel.h | 91 ++++ src/webchannel/qwebchannelglobal.h | 62 +++ src/webchannel/qwebsocketserver.cpp | 424 ++++++++++++++++++ src/webchannel/qwebsocketserver_p.h | 160 +++++++ src/webchannel/resources.qrc | 6 + src/webchannel/signalhandler_p.h | 278 ++++++++++++ src/webchannel/variantargument_p.h | 103 +++++ src/webchannel/webchannel.js | 129 ++++++ src/webchannel/webchannel.pro | 28 ++ 35 files changed, 2821 insertions(+), 2713 deletions(-) create mode 100644 src/imports/imports.pro create mode 100644 src/imports/webchannel/plugin.cpp create mode 100644 src/imports/webchannel/plugins.qmltypes create mode 100644 src/imports/webchannel/qmldir create mode 100644 src/imports/webchannel/webchannel.pro delete mode 100644 src/plugin.json delete mode 100644 src/qmldir delete mode 100644 src/qobject.js delete mode 100644 src/qtmetaobjectpublisher.cpp delete mode 100644 src/qtmetaobjectpublisher.h delete mode 100644 src/qwebchannel.cpp delete mode 100644 src/qwebchannel.h delete mode 100644 src/qwebchannel_plugin.cpp delete mode 100644 src/qwebchannel_plugin.h delete mode 100644 src/qwebsocketserver.cpp delete mode 100644 src/qwebsocketserver.h delete mode 100644 src/resources.qrc delete mode 100644 src/signalhandler.h delete mode 100644 src/src.pri delete mode 100644 src/variantargument.h delete mode 100644 src/webchannel.js create mode 100644 src/webchannel/qmetaobjectpublisher.cpp create mode 100644 src/webchannel/qmetaobjectpublisher.h create mode 100644 src/webchannel/qobject.js create mode 100644 src/webchannel/qwebchannel.cpp create mode 100644 src/webchannel/qwebchannel.h create mode 100644 src/webchannel/qwebchannelglobal.h create mode 100644 src/webchannel/qwebsocketserver.cpp create mode 100644 src/webchannel/qwebsocketserver_p.h create mode 100644 src/webchannel/resources.qrc create mode 100644 src/webchannel/signalhandler_p.h create mode 100644 src/webchannel/variantargument_p.h create mode 100644 src/webchannel/webchannel.js create mode 100644 src/webchannel/webchannel.pro (limited to 'src') diff --git a/src/imports/imports.pro b/src/imports/imports.pro new file mode 100644 index 0000000..657e144 --- /dev/null +++ b/src/imports/imports.pro @@ -0,0 +1,3 @@ +TEMPLATE = subdirs + +SUBDIRS += webchannel diff --git a/src/imports/webchannel/plugin.cpp b/src/imports/webchannel/plugin.cpp new file mode 100644 index 0000000..a5e9895 --- /dev/null +++ b/src/imports/webchannel/plugin.cpp @@ -0,0 +1,69 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Copyright (C) 2013 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff +** Contact: http://www.qt-project.org/legal +* +** This file is part of the QtWebChannel 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 Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/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 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include +#include + +#include "qwebchannel.h" +#include "qmetaobjectpublisher.h" + +QT_USE_NAMESPACE + +class QWebChannelPlugin : public QQmlExtensionPlugin +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QQmlExtensionInterface") + +public: + void registerTypes(const char *uri); +}; + +void QWebChannelPlugin::registerTypes(const char *uri) +{ + int major = 1; + int minor = 0; + qmlRegisterType(uri, major, minor, "WebChannel"); + qmlRegisterType(uri, major, minor, "MetaObjectPublisher"); + +} + +#include "plugin.moc" diff --git a/src/imports/webchannel/plugins.qmltypes b/src/imports/webchannel/plugins.qmltypes new file mode 100644 index 0000000..6d08f9c --- /dev/null +++ b/src/imports/webchannel/plugins.qmltypes @@ -0,0 +1,98 @@ +import QtQuick.tooling 1.1 + +// This file describes the plugin-supplied types contained in the library. +// It is used for QML tooling purposes only. +// +// This file was auto-generated by: +// 'qmlplugindump -notrelocatable QtWebChannel 5.3' + +Module { + Component { + name: "QMetaObjectPublisher" + prototype: "QObject" + exports: ["QtWebChannel/MetaObjectPublisher 5.3"] + exportMetaObjectRevisions: [0] + Property { name: "webChannel"; type: "QWebChannel"; isPointer: true } + Property { name: "blockUpdates"; type: "bool" } + Signal { + name: "webChannelChanged" + Parameter { name: "channel"; type: "QWebChannel"; isPointer: true } + } + Signal { + name: "blockUpdatesChanged" + Parameter { name: "block"; type: "bool" } + } + Method { + name: "classInfoForObjects" + type: "QVariantMap" + Parameter { name: "objects"; type: "QVariantMap" } + } + Method { + name: "classInfoForObject" + type: "QVariantMap" + Parameter { name: "object"; type: "QObject"; isPointer: true } + } + Method { + name: "registerObjects" + Parameter { name: "objects"; type: "QVariantMap" } + } + Method { + name: "handleRequest" + type: "bool" + Parameter { name: "message"; type: "QJsonObject" } + } + Method { name: "bench_ensureUpdatesInitialized" } + Method { name: "bench_sendPendingPropertyUpdates" } + Method { + name: "bench_registerObjects" + Parameter { name: "objects"; type: "QVariantMap" } + } + Method { name: "bench_initializeClients" } + Method { name: "test_clientIsIdle"; type: "bool" } + } + Component { + name: "QWebChannel" + prototype: "QObject" + exports: ["QtWebChannel/WebChannel 5.3"] + exportMetaObjectRevisions: [0] + Property { name: "baseUrl"; type: "string"; isReadonly: true } + Property { name: "useSecret"; type: "bool" } + Signal { + name: "baseUrlChanged" + Parameter { name: "baseUrl"; type: "string" } + } + Signal { + name: "rawMessageReceived" + Parameter { name: "rawMessage"; type: "string" } + } + Signal { name: "pongReceived" } + Signal { name: "initialized" } + Signal { + name: "failed" + Parameter { name: "reason"; type: "string" } + } + Method { + name: "sendMessage" + Parameter { name: "id"; type: "QJsonValue" } + Parameter { name: "data"; type: "QJsonValue" } + } + Method { + name: "sendMessage" + Parameter { name: "id"; type: "QJsonValue" } + } + Method { + name: "respond" + Parameter { name: "messageId"; type: "QJsonValue" } + Parameter { name: "data"; type: "QJsonValue" } + } + Method { + name: "respond" + Parameter { name: "messageId"; type: "QJsonValue" } + } + Method { + name: "sendRawMessage" + Parameter { name: "rawMessage"; type: "string" } + } + Method { name: "ping" } + } +} diff --git a/src/imports/webchannel/qmldir b/src/imports/webchannel/qmldir new file mode 100644 index 0000000..17184fe --- /dev/null +++ b/src/imports/webchannel/qmldir @@ -0,0 +1,3 @@ +module QtWebChannel +plugin declarative_webchannel +typeinfo plugins.qmltypes diff --git a/src/imports/webchannel/webchannel.pro b/src/imports/webchannel/webchannel.pro new file mode 100644 index 0000000..1753cee --- /dev/null +++ b/src/imports/webchannel/webchannel.pro @@ -0,0 +1,6 @@ +QT = core quick webchannel + +SOURCES += \ + plugin.cpp + +load(qml_plugin) diff --git a/src/plugin.json b/src/plugin.json deleted file mode 100644 index 39c1553..0000000 --- a/src/plugin.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "Keys": [ "QWebChannel" ] -} diff --git a/src/qmldir b/src/qmldir deleted file mode 100644 index ebd912a..0000000 --- a/src/qmldir +++ /dev/null @@ -1,2 +0,0 @@ -module Qt.labs.WebChannel -plugin qwebchannel diff --git a/src/qobject.js b/src/qobject.js deleted file mode 100644 index b783a5b..0000000 --- a/src/qobject.js +++ /dev/null @@ -1,307 +0,0 @@ -/**************************************************************************** -** -** Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies). -** All rights reserved. -** Contact: Nokia Corporation (qt-info@nokia.com) -** -** Copyright (C) 2013 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff -** Contact: http://www.qt-project.org/legal -** -** This file is part of the QWebChannel module on Qt labs. -** -** $QT_BEGIN_LICENSE:LGPL$ -** No Commercial Usage -** This file contains pre-release code and may not be distributed. -** You may use this file in accordance with the terms and conditions -** contained in the Technology Preview License Agreement accompanying -** this package. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 2.1 as published by the Free Software -** Foundation and appearing in the file LICENSE.LGPL included in the -** packaging of this file. Please review the following information to -** ensure the GNU Lesser General Public License version 2.1 requirements -** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. -** -** In addition, as a special exception, Nokia gives you certain additional -** rights. These rights are described in the Nokia Qt LGPL Exception -** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. -** -** If you have questions regarding the use of this file, please contact -** Nokia at qt-info@nokia.com. -** -** -** -** -** -** -** -** -** $QT_END_LICENSE$ -** -****************************************************************************/ - -"use strict"; - -function QObject(name, data, webChannel) -{ - this.__id__ = name; - webChannel.objectMap[name] = this; - - // List of callbacks that get invoked upon signal emission - this.__objectSignals__ = {}; - - // Cache of all properties, updated when a notify signal is emitted - this.__propertyCache__ = {}; - - var object = this; - - // ---------------------------------------------------------------------- - - function unwrapQObject( response ) - { - if (!response["__QObject*__"] - || response["id"] === undefined - || response["data"] === undefined) { - return response; - } - var objectId = response.id; - if (webChannel.objectMap[objectId]) - return webChannel.objectMap[objectId]; - - var qObject = new QObject( objectId, response.data, webChannel ); - qObject.destroyed.connect(function() { - if (webChannel.objectMap[objectId] === qObject) { - delete webChannel.objectMap[objectId]; - // reset the now deleted QObject to an empty {} object - // just assigning {} though would not have the desired effect, but the - // below also ensures all external references will see the empty map - for (var prop in qObject) { - delete qObject[prop]; - } - } - }); - return qObject; - } - - function addSignal(signalData, isPropertyNotifySignal) - { - var signalName = signalData[0]; - var signalIndex = signalData[1]; - object[signalName] = { - connect: function(callback) { - if (typeof(callback) !== "function") { - console.error("Bad callback given to connect to signal " + signalName); - return; - } - - object.__objectSignals__[signalIndex] = object.__objectSignals__[signalIndex] || []; - object.__objectSignals__[signalIndex].push(callback); - - if (!isPropertyNotifySignal) { - // only required for "pure" signals, handled separately for properties in propertyUpdate - webChannel.exec({ - type: "Qt.connectToSignal", - object: object.__id__, - signal: signalIndex - }); - } - }, - disconnect: function(callback) { - if (typeof(callback) !== "function") { - console.error("Bad callback given to disconnect from signal " + signalName); - return; - } - object.__objectSignals__[signalIndex] = object.__objectSignals__[signalIndex] || []; - var idx = object.__objectSignals__[signalIndex].indexOf(callback); - if (idx === -1) { - console.error("Cannot find connection of signal " + signalName + " to " + callback.name); - return; - } - object.__objectSignals__[signalIndex].splice(idx, 1); - if (!isPropertyNotifySignal && object.__objectSignals__[signalIndex].length === 0) { - // only required for "pure" signals, handled separately for properties in propertyUpdate - webChannel.exec({ - type: "Qt.disconnectFromSignal", - object: object.__id__, - signal: signalIndex - }); - } - } - }; - } - - /** - * Invokes all callbacks for the given signalname. Also works for property notify callbacks. - */ - function invokeSignalCallbacks(signalName, signalArgs) - { - var connections = object.__objectSignals__[signalName]; - if (connections) { - connections.forEach(function(callback) { - callback.apply(callback, signalArgs); - }); - } - } - - this.propertyUpdate = function(signals, propertyMap) - { - // update property cache - for (var propertyName in propertyMap) { - var propertyValue = propertyMap[propertyName]; - object.__propertyCache__[propertyName] = propertyValue; - } - - for (var signalName in signals) { - // Invoke all callbacks, as signalEmitted() does not. This ensures the - // property cache is updated before the callbacks are invoked. - invokeSignalCallbacks(signalName, signals[signalName]); - } - } - - this.signalEmitted = function(signalName, signalArgs) - { - invokeSignalCallbacks(signalName, signalArgs); - } - - function addMethod(methodData) - { - var methodName = methodData[0]; - var methodIdx = methodData[1]; - object[methodName] = function() { - var args = []; - var callback; - for (var i = 0; i < arguments.length; ++i) { - if (typeof arguments[i] === "function") - callback = arguments[i]; - else - args.push(arguments[i]); - } - - webChannel.exec({"type": "Qt.invokeMethod", "object": object.__id__, "method": methodIdx, "args": args}, function(response) { - if ( (response !== undefined) && callback ) { - (callback)(unwrapQObject(response)); - } - }); - }; - } - - function bindGetterSetter(propertyInfo) - { - var propertyName = propertyInfo[0]; - var notifySignalData = propertyInfo[1]; - // initialize property cache with current value - object.__propertyCache__[propertyName] = propertyInfo[2] - - if (notifySignalData) { - if (notifySignalData[0] === 1) { - // signal name is optimized away, reconstruct the actual name - notifySignalData[0] = propertyName + "Changed"; - } - addSignal(notifySignalData, true); - } - - object.__defineSetter__(propertyName, function(value) { - if (value === undefined) { - console.warn("Property setter for " + propertyName + " called with undefined value!"); - return; - } - object.__propertyCache__[propertyName] = value; - webChannel.exec({"type": "Qt.setProperty", "object": object.__id__, "property": propertyName, "value": value }); - - }); - object.__defineGetter__(propertyName, function () { - return (function (callback) { - var propertyValue = object.__propertyCache__[propertyName]; - if (propertyValue === undefined) { - // This shouldn't happen - console.warn("Undefined value in property cache for property \"" + propertyName + "\" in object " + object.__id__); - } - - // TODO: A callback is not required here anymore, but is kept for backwards compatibility - if (callback !== undefined) { - if (typeof(callback) !== "function") { - console.error("Bad callback given to get property " + property); - return; - } - callback(propertyValue); - } else { - return propertyValue; - } - }); - }); - } - - // ---------------------------------------------------------------------- - - data.methods.forEach(addMethod); - - data.properties.forEach(bindGetterSetter); - - data.signals.forEach(function(signal) { addSignal(signal, false); }); - - for (var name in data.enums) { - object[name] = data.enums[name]; - } -} - -window.setupQObjectWebChannel = function(webChannel, doneCallback) -{ - // prevent multiple initialization which might happen with multiple webchannel clients. - var initialized = false; - - webChannel.subscribe( - "Qt.signal", - function(payload) { - var object = window[payload.object] || webChannel.objectMap[payload.object]; - if (object) { - object.signalEmitted(payload.signal, payload.args); - } else { - console.warn("Unhandled signal: " + payload.object + "::" + payload.signal); - } - } - ); - - webChannel.subscribe( - "Qt.propertyUpdate", - function(payload) { - for (var i in payload) { - var data = payload[i]; - var object = window[data.object] || webChannel.objectMap[data.object]; - if (object) { - object.propertyUpdate(data.signals, data.properties); - } else { - console.warn("Unhandled property update: " + data.object + "::" + data.signal); - } - } - setTimeout(function() { webChannel.exec({type: "Qt.idle"}); }, 0); - } - ); - - webChannel.subscribe( - "Qt.init", - function(payload) { - if (initialized) { - return; - } - initialized = true; - for (var objectName in payload) { - var data = payload[objectName]; - var object = new QObject(objectName, data, webChannel); - window[objectName] = object; - } - if (doneCallback) { - doneCallback(); - } - setTimeout(function() { webChannel.exec({type: "Qt.idle"}); }, 0); - } - ); - - webChannel.exec({type:"Qt.init"}); - - webChannel.debug = function(message) - { - webChannel.send({"data" : {"type" : "Qt.Debug", "message" : message}}); - }; -}; diff --git a/src/qtmetaobjectpublisher.cpp b/src/qtmetaobjectpublisher.cpp deleted file mode 100644 index 9ca0d93..0000000 --- a/src/qtmetaobjectpublisher.cpp +++ /dev/null @@ -1,731 +0,0 @@ -/**************************************************************************** -** -** Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies). -** All rights reserved. -** Contact: Nokia Corporation (qt-info@nokia.com) -** -** Copyright (C) 2013 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff -** Contact: http://www.qt-project.org/legal -** -** This file is part of the QWebChannel module on Qt labs. -** -** $QT_BEGIN_LICENSE:LGPL$ -** No Commercial Usage -** This file contains pre-release code and may not be distributed. -** You may use this file in accordance with the terms and conditions -** contained in the Technology Preview License Agreement accompanying -** this package. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 2.1 as published by the Free Software -** Foundation and appearing in the file LICENSE.LGPL included in the -** packaging of this file. Please review the following information to -** ensure the GNU Lesser General Public License version 2.1 requirements -** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. -** -** In addition, as a special exception, Nokia gives you certain additional -** rights. These rights are described in the Nokia Qt LGPL Exception -** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. -** -** If you have questions regarding the use of this file, please contact -** Nokia at qt-info@nokia.com. -** -** -** -** -** -** -** -** -** $QT_END_LICENSE$ -** -****************************************************************************/ - -#include "qtmetaobjectpublisher.h" -#include "qwebchannel.h" -#include "variantargument.h" -#include "signalhandler.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -namespace { -const QString KEY_SIGNALS = QStringLiteral("signals"); -const QString KEY_METHODS = QStringLiteral("methods"); -const QString KEY_PROPERTIES = QStringLiteral("properties"); -const QString KEY_ENUMS = QStringLiteral("enums"); -const QString KEY_QOBJECT = QStringLiteral("__QObject*__"); -const QString KEY_ID = QStringLiteral("id"); -const QString KEY_DATA = QStringLiteral("data"); -const QString KEY_OBJECT = QStringLiteral("object"); -const QString KEY_DESTROYED = QStringLiteral("destroyed"); -const QString KEY_SIGNAL = QStringLiteral("signal"); -const QString KEY_TYPE = QStringLiteral("type"); -const QString KEY_MESSAGE = QStringLiteral("message"); -const QString KEY_METHOD = QStringLiteral("method"); -const QString KEY_ARGS = QStringLiteral("args"); -const QString KEY_PROPERTY = QStringLiteral("property"); -const QString KEY_VALUE = QStringLiteral("value"); - -const QString TYPE_SIGNAL = QStringLiteral("Qt.signal"); -const QString TYPE_PROPERTY_UPDATE = QStringLiteral("Qt.propertyUpdate"); -const QString TYPE_INIT = QStringLiteral("Qt.init"); -const QString TYPE_IDLE = QStringLiteral("Qt.idle"); -const QString TYPE_DEBUG = QStringLiteral("Qt.debug"); -const QString TYPE_INVOKE_METHOD = QStringLiteral("Qt.invokeMethod"); -const QString TYPE_CONNECT_TO_SIGNAL = QStringLiteral("Qt.connectToSignal"); -const QString TYPE_DISCONNECT_FROM_SIGNAL = QStringLiteral("Qt.disconnectFromSignal"); -const QString TYPE_SET_PROPERTY = QStringLiteral("Qt.setProperty"); - -QString objectId(const QObject *object) -{ - return QString::number(quintptr(object), 16); -} - -const int s_destroyedSignalIndex = QObject::staticMetaObject.indexOfMethod("destroyed(QObject*)"); - -/// TODO: what is the proper value here? -const int PROPERTY_UPDATE_INTERVAL = 50; -} - -struct QtMetaObjectPublisherPrivate -{ - QtMetaObjectPublisherPrivate(QtMetaObjectPublisher *q) - : q(q) - , signalHandler(this) - , clientIsIdle(false) - , blockUpdates(false) - , pendingInit(false) - , propertyUpdatesInitialized(false) - { - } - - /** - * Set the client to idle or busy, based on the value of @p isIdle. - * - * When the value changed, start/stop the property update timer accordingly. - */ - void setClientIsIdle(bool isIdle); - - /** - * Initialize clients by sending them the class information of the registered objects. - * - * Furthermore, if that was not done already, connect to their property notify signals. - */ - void initializeClients(); - - /** - * Go through all properties of the given object and connect to their notify signal. - * - * When receiving a notify signal, it will store the information in pendingPropertyUpdates which - * gets send via a Qt.propertyUpdate message to the server when the grouping timer timeouts. - */ - void initializePropertyUpdates(const QObject *const object, const QVariantMap &objectInfo); - - /** - * Send the clients the new property values since the last time this function was invoked. - * - * This is a grouped batch of all properties for which their notify signal was emitted. - * The list of signals as well as the arguments they contained, are also transmitted to - * the remote clients. - * - * @sa timer, initializePropertyUpdates - */ - void sendPendingPropertyUpdates(); - - /** - * Invoke the method of index @p methodIndex on @p object with the arguments @p args. - * - * The return value of the method invocation is then transmitted to the calling client - * via a webchannel response to the message identified by @p id. - */ - bool invokeMethod(QObject *const object, const int methodIndex, const QJsonArray &args, const QJsonValue &id); - - /** - * Callback of the signalHandler which forwards the signal invocation to the webchannel clients. - */ - void signalEmitted(const QObject *object, const int signalIndex, const QVariantList &arguments); - - /** - * Callback for registered or wrapped objects which erases all data related to @p object. - * - * @sa signalEmitted - */ - void objectDestroyed(const QObject *object); - - /** - * Given a QVariant containing a QObject*, wrap the object and register for property updates - * return the objects class information. - * - * All other input types are returned as-is. - * - * TODO: support wrapping of initially-registered objects - */ - QVariant wrapResult(const QVariant &result); - - /** - * Invoke delete later on @p object. - */ - void deleteWrappedObject(QObject *object) const; - - QtMetaObjectPublisher *q; - QPointer webChannel; - SignalHandler signalHandler; - - // true when the client is idle, false otherwise - bool clientIsIdle; - - // true when no property updates should be sent, false otherwise - bool blockUpdates; - - // true when at least one client needs to be initialized, - // i.e. when a Qt.init came in which was not handled yet. - bool pendingInit; - - // true when at least one client was initialized and thus - // the property updates have been initialized and the - // object info map set. - bool propertyUpdatesInitialized; - - // Map of registered objects indexed by their id. - QHash registeredObjects; - - // Map the registered objects to their id. - QHash registeredObjectIds; - - // Map of object names to maps of signal indices to a set of all their properties. - // The last value is a set as a signal can be the notify signal of multiple properties. - typedef QHash > SignalToPropertyNameMap; - QHash signalToPropertyMap; - - // Objects that changed their properties and are waiting for idle client. - // map of object name to map of signal index to arguments - typedef QHash SignalToArgumentsMap; - typedef QHash PendingPropertyUpdates; - PendingPropertyUpdates pendingPropertyUpdates; - - // Maps wrapped object to class info - QHash wrappedObjects; - - // Aggregate property updates since we get multiple Qt.idle message when we have multiple - // clients. They all share the same QWebProcess though so we must take special care to - // prevent message flooding. - QBasicTimer timer; -}; - -void QtMetaObjectPublisherPrivate::setClientIsIdle(bool isIdle) -{ - if (clientIsIdle == isIdle) { - return; - } - clientIsIdle = isIdle; - if (!isIdle && timer.isActive()) { - timer.stop(); - } else if (isIdle && !timer.isActive()) { - timer.start(PROPERTY_UPDATE_INTERVAL, q); - } -} - -void QtMetaObjectPublisherPrivate::initializeClients() -{ - QJsonObject objectInfos; - { - const QHash::const_iterator end = registeredObjects.constEnd(); - for (QHash::const_iterator it = registeredObjects.constBegin(); it != end; ++it) { - const QVariantMap &info = q->classInfoForObject(it.value()); - if (!propertyUpdatesInitialized) { - initializePropertyUpdates(it.value(), info); - } - objectInfos[it.key()] = QJsonObject::fromVariantMap(info); - } - } - webChannel->sendMessage(TYPE_INIT, objectInfos); - propertyUpdatesInitialized = true; - pendingInit = false; -} - -void QtMetaObjectPublisherPrivate::initializePropertyUpdates(const QObject *const object, const QVariantMap &objectInfo) -{ - foreach (const QVariant &propertyInfoVar, objectInfo[KEY_PROPERTIES].toList()) { - const QVariantList &propertyInfo = propertyInfoVar.toList(); - if (propertyInfo.size() < 2) { - qWarning() << "Invalid property info encountered:" << propertyInfoVar; - continue; - } - const QString &propertyName = propertyInfo.at(0).toString(); - const QVariantList &signalData = propertyInfo.at(1).toList(); - - if (signalData.isEmpty()) { - // Property without NOTIFY signal - continue; - } - - const int signalIndex = signalData.at(1).toInt(); - - QSet &connectedProperties = signalToPropertyMap[object][signalIndex]; - - // Only connect for a property update once - if (connectedProperties.isEmpty()) { - signalHandler.connectTo(object, signalIndex); - } - - connectedProperties.insert(propertyName); - } - - // also always connect to destroyed signal - signalHandler.connectTo(object, s_destroyedSignalIndex); -} - -void QtMetaObjectPublisherPrivate::sendPendingPropertyUpdates() -{ - if (blockUpdates || !clientIsIdle || pendingPropertyUpdates.isEmpty()) { - return; - } - - QJsonArray data; - - // convert pending property updates to JSON data - const PendingPropertyUpdates::const_iterator end = pendingPropertyUpdates.constEnd(); - for (PendingPropertyUpdates::const_iterator it = pendingPropertyUpdates.constBegin(); it != end; ++it) { - const QObject *object = it.key(); - const QMetaObject *const metaObject = object->metaObject(); - const SignalToPropertyNameMap &objectsSignalToPropertyMap = signalToPropertyMap.value(object); - // maps property name to current property value - QJsonObject properties; - // maps signal index to list of arguments of the last emit - QJsonObject sigs; - const SignalToArgumentsMap::const_iterator sigEnd = it.value().constEnd(); - for (SignalToArgumentsMap::const_iterator sigIt = it.value().constBegin(); sigIt != sigEnd; ++sigIt) { - // TODO: use property indices - foreach (const QString &propertyName, objectsSignalToPropertyMap.value(sigIt.key())) { - int propertyIndex = metaObject->indexOfProperty(qPrintable(propertyName)); - if (propertyIndex == -1) { - qWarning("Unknown property %d encountered", propertyIndex); - continue; - } - const QMetaProperty &property = metaObject->property(propertyIndex); - properties[QString::fromLatin1(property.name())] = QJsonValue::fromVariant(property.read(object)); - } - // TODO: can we get rid of the int <-> string conversions here? - sigs[QString::number(sigIt.key())] = QJsonArray::fromVariantList(sigIt.value()); - } - QJsonObject obj; - obj[KEY_OBJECT] = registeredObjectIds.value(object); - obj[KEY_SIGNALS] = sigs; - obj[KEY_PROPERTIES] = properties; - data.push_back(obj); - } - - pendingPropertyUpdates.clear(); - webChannel->sendMessage(TYPE_PROPERTY_UPDATE, data); - setClientIsIdle(false); -} - -bool QtMetaObjectPublisherPrivate::invokeMethod(QObject *const object, const int methodIndex, - const QJsonArray &args, const QJsonValue &id) -{ - const QMetaMethod &method = object->metaObject()->method(methodIndex); - - if (method.name() == QByteArrayLiteral("deleteLater")) { - // invoke `deleteLater` on wrapped QObject indirectly - deleteWrappedObject(object); - return true; - } else if (!method.isValid()) { - qWarning() << "Cannot invoke unknown method of index" << methodIndex << "on object" << object << '.'; - return false; - } else if (method.access() != QMetaMethod::Public) { - qWarning() << "Cannot invoke non-public method" << method.name() << "on object" << object << '.'; - return false; - } else if (method.methodType() != QMetaMethod::Method && method.methodType() != QMetaMethod::Slot) { - qWarning() << "Cannot invoke non-public method" << method.name() << "on object" << object << '.'; - return false; - } else if (args.size() > 10) { - qWarning() << "Cannot invoke method" << method.name() << "on object" << object << "with more than 10 arguments, as that is not supported by QMetaMethod::invoke."; - return false; - } else if (args.size() > method.parameterCount()) { - qWarning() << "Ignoring additional arguments while invoking method" << method.name() << "on object" << object << ':' - << args.size() << "arguments given, but method only takes" << method.parameterCount() << '.'; - } - - // construct converter objects of QVariant to QGenericArgument - VariantArgument arguments[10]; - for (int i = 0; i < qMin(args.size(), method.parameterCount()); ++i) { - arguments[i].setValue(args.at(i).toVariant(), method.parameterType(i)); - } - - // construct QGenericReturnArgument - QVariant returnValue; - if (method.returnType() != qMetaTypeId() && method.returnType() != qMetaTypeId()) { - // Only init variant with return type if its not a variant itself, which would - // lead to nested variants which is not what we want. - // Also, skip void-return types for obvious reasons (and to prevent a runtime warning inside Qt). - returnValue = QVariant(method.returnType(), 0); - } - QGenericReturnArgument returnArgument(method.typeName(), returnValue.data()); - - // now we can call the method - method.invoke(object, returnArgument, - arguments[0], arguments[1], arguments[2], arguments[3], arguments[4], - arguments[5], arguments[6], arguments[7], arguments[8], arguments[9]); - - // and send the return value to the client - webChannel->respond(id, QJsonValue::fromVariant(wrapResult(returnValue))); - - return true; -} - -void QtMetaObjectPublisherPrivate::signalEmitted(const QObject *object, const int signalIndex, const QVariantList &arguments) -{ - if (!webChannel) { - return; - } - if (!signalToPropertyMap.value(object).contains(signalIndex)) { - QJsonObject data; - const QString &objectName = registeredObjectIds.value(object); - Q_ASSERT(!objectName.isEmpty()); - data[KEY_OBJECT] = objectName; - data[KEY_SIGNAL] = signalIndex; - if (!arguments.isEmpty()) { - // TODO: wrap (new) objects on the fly - data[KEY_ARGS] = QJsonArray::fromVariantList(arguments); - } - webChannel->sendMessage(TYPE_SIGNAL, data); - - if (signalIndex == s_destroyedSignalIndex) { - objectDestroyed(object); - } - } else { - pendingPropertyUpdates[object][signalIndex] = arguments; - if (clientIsIdle && !blockUpdates && !timer.isActive()) { - timer.start(PROPERTY_UPDATE_INTERVAL, q); - } - } -} - -void QtMetaObjectPublisherPrivate::objectDestroyed(const QObject *object) -{ - const QString &id = registeredObjectIds.take(object); - Q_ASSERT(!id.isEmpty()); - bool removed = registeredObjects.remove(id); - Q_ASSERT(removed); - Q_UNUSED(removed); - - signalToPropertyMap.remove(object); - pendingPropertyUpdates.remove(object); - wrappedObjects.remove(object); -} - -QVariant QtMetaObjectPublisherPrivate::wrapResult(const QVariant &result) -{ - if (QObject *object = result.value()) { - QVariantMap &objectInfo = wrappedObjects[object]; - if (!objectInfo.isEmpty()) { - // already registered, use cached information - Q_ASSERT(registeredObjectIds.contains(object)); - return objectInfo; - } // else the object is not yet wrapped, do it now - - const QString &id = objectId(object); - Q_ASSERT(!registeredObjects.contains(id)); - Q_ASSERT(!registeredObjectIds.contains(object)); - - objectInfo[KEY_QOBJECT] = true; - objectInfo[KEY_ID] = id; - objectInfo[KEY_DATA] = q->classInfoForObject(object); - - registeredObjectIds[object] = id; - registeredObjects[id] = object; - wrappedObjects.insert(object, objectInfo); - - initializePropertyUpdates(object, objectInfo); - return objectInfo; - } - - // no need to wrap this - return result; -} - -void QtMetaObjectPublisherPrivate::deleteWrappedObject(QObject *object) const -{ - if (!wrappedObjects.contains(object)) { - qWarning() << "Not deleting non-wrapped object" << object; - return; - } - object->deleteLater(); -} - -QtMetaObjectPublisher::QtMetaObjectPublisher(QObject *parent) - : QObject(parent) - , d(new QtMetaObjectPublisherPrivate(this)) -{ -} - -QtMetaObjectPublisher::~QtMetaObjectPublisher() -{ - -} - -QVariantMap QtMetaObjectPublisher::classInfoForObjects(const QVariantMap &objectMap) const -{ - QVariantMap ret; - QMap::const_iterator it = objectMap.constBegin(); - while (it != objectMap.constEnd()) { - QObject *object = it.value().value(); - if (object) { - const QVariantMap &info = classInfoForObject(object); - if (!info.isEmpty()) { - ret[it.key()] = info; - } - } - ++it; - } - return ret; -} - -QVariantMap QtMetaObjectPublisher::classInfoForObject(QObject *object) const -{ - QVariantMap data; - if (!object) { - qWarning("null object given to MetaObjectPublisher - bad API usage?"); - return data; - } - QVariantList qtSignals, qtMethods; - QVariantList qtProperties; - QVariantMap qtEnums; - const QMetaObject *metaObject = object->metaObject(); - QSet notifySignals; - QSet identifiers; - for (int i = 0; i < metaObject->propertyCount(); ++i) { - const QMetaProperty &prop = metaObject->property(i); - QVariantList propertyInfo; - const QString &propertyName = QString::fromLatin1(prop.name()); - propertyInfo.append(propertyName); - identifiers << propertyName; - if (prop.hasNotifySignal()) { - notifySignals << prop.notifySignalIndex(); - const int numParams = prop.notifySignal().parameterCount(); - if (numParams > 1) { - qWarning("Notify signal for property '%s' has %d parameters, expected zero or one.", - prop.name(), numParams); - } - // optimize: compress the common propertyChanged notification names, just send a 1 - const QByteArray ¬ifySignal = prop.notifySignal().name(); - static const QByteArray changedSuffix = QByteArrayLiteral("Changed"); - if (notifySignal.length() == changedSuffix.length() + propertyName.length() && - notifySignal.endsWith(changedSuffix) && notifySignal.startsWith(prop.name())) - { - propertyInfo.append(QVariant::fromValue(QVariantList() << 1 << prop.notifySignalIndex())); - } else { - propertyInfo.append(QVariant::fromValue(QVariantList() << QString::fromLatin1(notifySignal) << prop.notifySignalIndex())); - } - } else { - if (!prop.isConstant()) { - qWarning("Property '%s'' of object '%s' has no notify signal and is not constant, " - "value updates in HTML will be broken!", - prop.name(), object->metaObject()->className()); - } - propertyInfo.append(QVariant::fromValue(QVariantList())); - } - propertyInfo.append(prop.read(object)); - qtProperties.append(QVariant::fromValue(propertyInfo)); - } - for (int i = 0; i < metaObject->methodCount(); ++i) { - if (notifySignals.contains(i)) { - continue; - } - const QMetaMethod &method = metaObject->method(i); - //NOTE: this must be a string, otherwise it will be converted to '{}' in QML - const QString &name = QString::fromLatin1(method.name()); - // optimize: skip overloaded methods/signals or property getters, on the JS side we can only - // call one of them anyways - // TODO: basic support for overloaded signals, methods - if (identifiers.contains(name)) { - continue; - } - identifiers << name; - // send data as array to client with format: [name, index] - const QVariant data = QVariant::fromValue(QVariantList() << name << i); - if (method.methodType() == QMetaMethod::Signal) { - qtSignals.append(data); - } else if (method.access() == QMetaMethod::Public) { - qtMethods.append(data); - } - } - for (int i = 0; i < metaObject->enumeratorCount(); ++i) { - QMetaEnum enumerator = metaObject->enumerator(i); - QVariantMap values; - for (int k = 0; k < enumerator.keyCount(); ++k) { - values[enumerator.key(k)] = enumerator.value(k); - } - qtEnums[enumerator.name()] = values; - } - data[KEY_SIGNALS] = qtSignals; - data[KEY_METHODS] = qtMethods; - data[KEY_PROPERTIES] = QVariant::fromValue(qtProperties); - data[KEY_ENUMS] = qtEnums; - return data; -} - -void QtMetaObjectPublisher::registerObjects(const QVariantMap &objects) -{ - if (d->propertyUpdatesInitialized) { - qWarning("Registered new object after initialization. This does not work!"); - return; - } - const QMap::const_iterator end = objects.end(); - for (QMap::const_iterator it = objects.begin(); it != end; ++it) { - QObject *object = it.value().value(); - if (!object) { - qWarning("Invalid QObject given to register under name %s", qPrintable(it.key())); - continue; - } - d->registeredObjects[it.key()] = object; - d->registeredObjectIds[object] = it.key(); - } -} - -bool QtMetaObjectPublisher::handleRequest(const QJsonObject &message) -{ - if (!message.contains(KEY_DATA)) { - return false; - } - - const QJsonObject &payload = message.value(KEY_DATA).toObject(); - if (!payload.contains(KEY_TYPE)) { - return false; - } - - const QString &type = payload.value(KEY_TYPE).toString(); - if (type == TYPE_IDLE) { - d->setClientIsIdle(true); - return true; - } else if (type == TYPE_INIT) { - if (!d->blockUpdates) { - d->initializeClients(); - } else { - d->pendingInit = true; - } - return true; - } else if (type == TYPE_DEBUG) { - static QTextStream out(stdout); - out << "DEBUG: " << payload.value(KEY_MESSAGE).toString() << endl; - return true; - } else if (payload.contains(KEY_OBJECT)) { - const QString &objectName = payload.value(KEY_OBJECT).toString(); - QObject *object = d->registeredObjects.value(objectName); - if (!object) { - qWarning() << "Unknown object encountered" << objectName; - return false; - } - - if (type == TYPE_INVOKE_METHOD) { - return d->invokeMethod(object, payload.value(KEY_METHOD).toInt(-1), payload.value(KEY_ARGS).toArray(), message.value(KEY_ID)); - } else if (type == TYPE_CONNECT_TO_SIGNAL) { - d->signalHandler.connectTo(object, payload.value(KEY_SIGNAL).toInt(-1)); - return true; - } else if (type == TYPE_DISCONNECT_FROM_SIGNAL) { - d->signalHandler.disconnectFrom(object, payload.value(KEY_SIGNAL).toInt(-1)); - return true; - } else if (type == TYPE_SET_PROPERTY) { - // TODO: use property indices - const QString &propertyName = payload.value(KEY_PROPERTY).toString(); - const int propertyIdx = object->metaObject()->indexOfProperty(qPrintable(propertyName)); - if (propertyIdx == -1) { - qWarning() << "Cannot set unknown property" << propertyName << "of object" << objectName; - return false; - } - object->metaObject()->property(propertyIdx).write(object, payload.value(KEY_VALUE).toVariant()); - return true; - } - } - return false; -} - -QWebChannel *QtMetaObjectPublisher::webChannel() const -{ - return d->webChannel; -} - -void QtMetaObjectPublisher::setWebChannel(QWebChannel *webChannel) -{ - if (d->webChannel == webChannel) { - return; - } - - d->webChannel = webChannel; - - emit webChannelChanged(webChannel); -} - -bool QtMetaObjectPublisher::blockUpdates() const -{ - return d->blockUpdates; -} - -void QtMetaObjectPublisher::setBlockUpdates(bool block) -{ - if (d->blockUpdates == block) { - return; - } - d->blockUpdates = block; - - if (!d->blockUpdates) { - if (d->pendingInit) { - d->initializeClients(); - } else { - d->sendPendingPropertyUpdates(); - } - } else if (d->timer.isActive()) { - d->timer.stop(); - } - - emit blockUpdatesChanged(block); -} - -void QtMetaObjectPublisher::bench_ensureUpdatesInitialized() -{ - if (!d->propertyUpdatesInitialized) { - d->initializeClients(); - } -} - -void QtMetaObjectPublisher::bench_sendPendingPropertyUpdates() -{ - d->clientIsIdle = true; - d->sendPendingPropertyUpdates(); -} - -void QtMetaObjectPublisher::bench_initializeClients() -{ - d->propertyUpdatesInitialized = false; - d->signalToPropertyMap.clear(); - d->signalHandler.clear(); - d->initializeClients(); -} - -void QtMetaObjectPublisher::bench_registerObjects(const QVariantMap &objects) -{ - d->propertyUpdatesInitialized = false; - registerObjects(objects); -} - -bool QtMetaObjectPublisher::test_clientIsIdle() const -{ - return d->clientIsIdle; -} - -void QtMetaObjectPublisher::timerEvent(QTimerEvent *event) -{ - if (event->timerId() == d->timer.timerId()) { - d->sendPendingPropertyUpdates(); - } else { - QObject::timerEvent(event); - } -} diff --git a/src/qtmetaobjectpublisher.h b/src/qtmetaobjectpublisher.h deleted file mode 100644 index 88a54c4..0000000 --- a/src/qtmetaobjectpublisher.h +++ /dev/null @@ -1,114 +0,0 @@ -/**************************************************************************** -** -** Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies). -** All rights reserved. -** Contact: Nokia Corporation (qt-info@nokia.com) -** -** Copyright (C) 2013 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff -** Contact: http://www.qt-project.org/legal -** -** This file is part of the QWebChannel module on Qt labs. -** -** $QT_BEGIN_LICENSE:LGPL$ -** No Commercial Usage -** This file contains pre-release code and may not be distributed. -** You may use this file in accordance with the terms and conditions -** contained in the Technology Preview License Agreement accompanying -** this package. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 2.1 as published by the Free Software -** Foundation and appearing in the file LICENSE.LGPL included in the -** packaging of this file. Please review the following information to -** ensure the GNU Lesser General Public License version 2.1 requirements -** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. -** -** In addition, as a special exception, Nokia gives you certain additional -** rights. These rights are described in the Nokia Qt LGPL Exception -** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. -** -** If you have questions regarding the use of this file, please contact -** Nokia at qt-info@nokia.com. -** -** -** -** -** -** -** -** -** $QT_END_LICENSE$ -** -****************************************************************************/ - -#ifndef QTMETAOBJECTPUBLISHER_H -#define QTMETAOBJECTPUBLISHER_H - -#include -#include - -class QWebChannel; - -struct QtMetaObjectPublisherPrivate; - -class QtMetaObjectPublisher : public QObject -{ - Q_OBJECT - Q_PROPERTY(QWebChannel *webChannel READ webChannel WRITE setWebChannel NOTIFY webChannelChanged); - Q_PROPERTY(bool blockUpdates READ blockUpdates WRITE setBlockUpdates NOTIFY blockUpdatesChanged); - -public: - explicit QtMetaObjectPublisher(QObject *parent = 0); - virtual ~QtMetaObjectPublisher(); - - Q_INVOKABLE QVariantMap classInfoForObjects(const QVariantMap &objects) const; - Q_INVOKABLE QVariantMap classInfoForObject(QObject *object) const; - - /** - * Register a map of string ID to QObject* objects. - * - * The properties, signals and public methods of the QObject are - * published to the remote client, where an object with the given identifier - * is constructed. - * - * TODO: This must be called, before clients are initialized. - */ - Q_INVOKABLE void registerObjects(const QVariantMap &objects); - - /** - * Handle the given WebChannel client request and potentially give a response. - * - * @return true if the request was handled, false otherwise. - */ - Q_INVOKABLE bool handleRequest(const QJsonObject &message); - - QWebChannel *webChannel() const; - void setWebChannel(QWebChannel *webChannel); - - /** - * When updates are blocked, no property updates are transmitted to remote clients. - */ - bool blockUpdates() const; - void setBlockUpdates(bool block); - - /// TODO: cleanup: rewrite tests in C++ and access PIMPL data from there - Q_INVOKABLE void bench_ensureUpdatesInitialized(); - Q_INVOKABLE void bench_sendPendingPropertyUpdates(); - Q_INVOKABLE void bench_registerObjects(const QVariantMap &objects); - Q_INVOKABLE void bench_initializeClients(); - Q_INVOKABLE bool test_clientIsIdle() const; - -signals: - void webChannelChanged(QWebChannel *channel); - void blockUpdatesChanged(bool block); - -protected: - virtual void timerEvent(QTimerEvent *); - -private: - QScopedPointer d; - friend struct QtMetaObjectPublisherPrivate; -}; - -#endif // QTMETAOBJECTPUBLISHER_H diff --git a/src/qwebchannel.cpp b/src/qwebchannel.cpp deleted file mode 100644 index ac5f8f9..0000000 --- a/src/qwebchannel.cpp +++ /dev/null @@ -1,209 +0,0 @@ -/**************************************************************************** -** -** Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies). -** All rights reserved. -** Contact: Nokia Corporation (qt-info@nokia.com) -** -** Copyright (C) 2013 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff -** Contact: http://www.qt-project.org/legal -** -** This file is part of the QWebChannel module on Qt labs. -** -** $QT_BEGIN_LICENSE:LGPL$ -** No Commercial Usage -** This file contains pre-release code and may not be distributed. -** You may use this file in accordance with the terms and conditions -** contained in the Technology Preview License Agreement accompanying -** this package. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 2.1 as published by the Free Software -** Foundation and appearing in the file LICENSE.LGPL included in the -** packaging of this file. Please review the following information to -** ensure the GNU Lesser General Public License version 2.1 requirements -** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. -** -** In addition, as a special exception, Nokia gives you certain additional -** rights. These rights are described in the Nokia Qt LGPL Exception -** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. -** -** If you have questions regarding the use of this file, please contact -** Nokia at qt-info@nokia.com. -** -** -** -** -** -** -** -** -** $QT_END_LICENSE$ -** -****************************************************************************/ - -#include "qwebchannel.h" - -#include -#include -#include -#include -#include - -#include "qwebsocketserver.h" - -class QWebChannelPrivate : public QWebSocketServer -{ - Q_OBJECT -public: - QByteArray m_secret; - bool m_useSecret; - - QString m_baseUrl; - bool m_starting; - - QWebChannelPrivate(QObject* parent) - : QWebSocketServer(parent) - , m_useSecret(true) - , m_starting(false) - { - connect(this, SIGNAL(error(QAbstractSocket::SocketError)), - SLOT(socketError())); - } - - void initLater() - { - if (m_starting) - return; - metaObject()->invokeMethod(this, "init", Qt::QueuedConnection); - m_starting = true; - } - - void sendJSONMessage(const QJsonValue& id, const QJsonValue& data, bool response) const; - -signals: - void failed(const QString& reason); - void initialized(); - -protected: - virtual bool isValid(const HeaderData& connection); - -private slots: - void init(); - void socketError(); -}; - -bool QWebChannelPrivate::isValid(const HeaderData& connection) -{ - if (!QWebSocketServer::isValid(connection)) { - return false; - } - return connection.protocol == QByteArrayLiteral("QWebChannel") - && connection.path == m_secret; -} - -void QWebChannelPrivate::init() -{ - close(); - - m_starting = false; - if (m_useSecret) { - m_secret = QUuid::createUuid().toByteArray(); - // replace { by / - m_secret[0] = '/'; - // chop of trailing } - m_secret.chop(1); - } - - if (!listen(QHostAddress::LocalHost)) { - emit failed(errorString()); - return; - } - - m_baseUrl = QStringLiteral("127.0.0.1:%1%2").arg(port()).arg(QString::fromLatin1(m_secret)); - emit initialized(); -} - -void QWebChannelPrivate::socketError() -{ - emit failed(errorString()); -} - -void QWebChannelPrivate::sendJSONMessage(const QJsonValue& id, const QJsonValue& data, bool response) const -{ - QJsonObject obj; - if (response) { - obj[QStringLiteral("response")] = true; - } - obj[QStringLiteral("id")] = id; - if (!data.isNull()) { - obj[QStringLiteral("data")] = data; - } - QJsonDocument doc(obj); - sendMessage(doc.toJson(QJsonDocument::Compact)); -} - -QWebChannel::QWebChannel(QObject *parent) -: QObject(parent) -, d(new QWebChannelPrivate(this)) -{ - connect(d, SIGNAL(textDataReceived(QString)), - SIGNAL(rawMessageReceived(QString))); - connect(d, SIGNAL(failed(QString)), - SIGNAL(failed(QString))); - connect(d, SIGNAL(initialized()), - SLOT(onInitialized())); - connect(d, SIGNAL(pongReceived()), - SIGNAL(pongReceived())); - d->initLater(); -} - -QWebChannel::~QWebChannel() -{ -} - -QString QWebChannel::baseUrl() const -{ - return d->m_baseUrl; -} - -void QWebChannel::setUseSecret(bool s) -{ - if (d->m_useSecret == s) - return; - d->m_useSecret = s; - d->initLater(); -} - -bool QWebChannel::useSecret() const -{ - return d->m_useSecret; -} - -void QWebChannel::onInitialized() -{ - emit initialized(); - emit baseUrlChanged(d->m_baseUrl); -} - -void QWebChannel::respond(const QJsonValue& messageId, const QJsonValue& data) const -{ - d->sendJSONMessage(messageId, data, true); -} - -void QWebChannel::sendMessage(const QJsonValue& id, const QJsonValue& data) const -{ - d->sendJSONMessage(id, data, false); -} - -void QWebChannel::sendRawMessage(const QString& message) const -{ - d->sendMessage(message.toUtf8()); -} - -void QWebChannel::ping() const -{ - d->ping(); -} - -#include diff --git a/src/qwebchannel.h b/src/qwebchannel.h deleted file mode 100644 index 7f3ff13..0000000 --- a/src/qwebchannel.h +++ /dev/null @@ -1,91 +0,0 @@ -/**************************************************************************** -** -** Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies). -** All rights reserved. -** Contact: Nokia Corporation (qt-info@nokia.com) -** -** Copyright (C) 2013 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff -** Contact: http://www.qt-project.org/legal -** -** This file is part of the QWebChannel module on Qt labs. -** -** $QT_BEGIN_LICENSE:LGPL$ -** No Commercial Usage -** This file contains pre-release code and may not be distributed. -** You may use this file in accordance with the terms and conditions -** contained in the Technology Preview License Agreement accompanying -** this package. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 2.1 as published by the Free Software -** Foundation and appearing in the file LICENSE.LGPL included in the -** packaging of this file. Please review the following information to -** ensure the GNU Lesser General Public License version 2.1 requirements -** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. -** -** In addition, as a special exception, Nokia gives you certain additional -** rights. These rights are described in the Nokia Qt LGPL Exception -** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. -** -** If you have questions regarding the use of this file, please contact -** Nokia at qt-info@nokia.com. -** -** -** -** -** -** -** -** -** $QT_END_LICENSE$ -** -****************************************************************************/ - -#ifndef QWEBCHANNEL_H -#define QWEBCHANNEL_H - -#include -#include - -class QWebChannelPrivate; - -class QWebChannel : public QObject -{ - Q_OBJECT - Q_DISABLE_COPY(QWebChannel) - Q_PROPERTY(QString baseUrl READ baseUrl NOTIFY baseUrlChanged) - Q_PROPERTY(bool useSecret READ useSecret WRITE setUseSecret) - -public: - QWebChannel(QObject *parent = 0); - ~QWebChannel(); - - QString baseUrl() const; - - void setUseSecret(bool); - bool useSecret() const; - -signals: - void baseUrlChanged(const QString& baseUrl); - void rawMessageReceived(const QString& rawMessage); - void pongReceived(); - void initialized(); - - void failed(const QString& reason); - -public slots: - void sendMessage(const QJsonValue& id, const QJsonValue& data = QJsonValue()) const; - void respond(const QJsonValue& messageId, const QJsonValue& data = QJsonValue()) const; - void sendRawMessage(const QString& rawMessage) const; - void ping() const; - -private slots: - void onInitialized(); - -private: - QWebChannelPrivate* d; -}; - -#endif // QWEBCHANNEL_H - diff --git a/src/qwebchannel_plugin.cpp b/src/qwebchannel_plugin.cpp deleted file mode 100644 index 13a38a5..0000000 --- a/src/qwebchannel_plugin.cpp +++ /dev/null @@ -1,54 +0,0 @@ -/**************************************************************************** -** -** Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies). -** All rights reserved. -** Contact: Nokia Corporation (qt-info@nokia.com) -** -** This file is part of the QWebChannel module on Qt labs. -** -** $QT_BEGIN_LICENSE:LGPL$ -** No Commercial Usage -** This file contains pre-release code and may not be distributed. -** You may use this file in accordance with the terms and conditions -** contained in the Technology Preview License Agreement accompanying -** this package. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 2.1 as published by the Free Software -** Foundation and appearing in the file LICENSE.LGPL included in the -** packaging of this file. Please review the following information to -** ensure the GNU Lesser General Public License version 2.1 requirements -** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. -** -** In addition, as a special exception, Nokia gives you certain additional -** rights. These rights are described in the Nokia Qt LGPL Exception -** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. -** -** If you have questions regarding the use of this file, please contact -** Nokia at qt-info@nokia.com. -** -** -** -** -** -** -** -** -** $QT_END_LICENSE$ -** -****************************************************************************/ - -#include - -#include "qwebchannel.h" -#include "qtmetaobjectpublisher.h" - -#include "qwebchannel_plugin.h" - -void QWebChannelPlugin::registerTypes(const char *uri) -{ - qmlRegisterType(uri, 1, 0, "WebChannel"); - qmlRegisterType(uri, 1, 0, "MetaObjectPublisher"); -} - diff --git a/src/qwebchannel_plugin.h b/src/qwebchannel_plugin.h deleted file mode 100644 index a5b712d..0000000 --- a/src/qwebchannel_plugin.h +++ /dev/null @@ -1,56 +0,0 @@ -/**************************************************************************** -** -** Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies). -** All rights reserved. -** Contact: Nokia Corporation (qt-info@nokia.com) -** -** This file is part of the QWebChannel module on Qt labs. -** -** $QT_BEGIN_LICENSE:LGPL$ -** No Commercial Usage -** This file contains pre-release code and may not be distributed. -** You may use this file in accordance with the terms and conditions -** contained in the Technology Preview License Agreement accompanying -** this package. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 2.1 as published by the Free Software -** Foundation and appearing in the file LICENSE.LGPL included in the -** packaging of this file. Please review the following information to -** ensure the GNU Lesser General Public License version 2.1 requirements -** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. -** -** In addition, as a special exception, Nokia gives you certain additional -** rights. These rights are described in the Nokia Qt LGPL Exception -** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. -** -** If you have questions regarding the use of this file, please contact -** Nokia at qt-info@nokia.com. -** -** -** -** -** -** -** -** -** $QT_END_LICENSE$ -** -****************************************************************************/ - -#ifndef QWEBCHANNEL_PLUGIN_H -#define QWEBCHANNEL_PLUGIN_H - -#include - -class QWebChannelPlugin : public QQmlExtensionPlugin -{ - Q_OBJECT - Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QQmlExtensionInterface") -public: - void registerTypes(const char *uri); -}; - -#endif // QWEBCHANNEL_PLUGIN_H - diff --git a/src/qwebsocketserver.cpp b/src/qwebsocketserver.cpp deleted file mode 100644 index 6e9858b..0000000 --- a/src/qwebsocketserver.cpp +++ /dev/null @@ -1,422 +0,0 @@ -/**************************************************************************** -** -** Copyright (C) 2013 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff -** Contact: http://www.qt-project.org/legal -** -** This file is part of the QWebChannel module on Qt labs. -** -** $QT_BEGIN_LICENSE:LGPL$ -** No Commercial Usage -** This file contains pre-release code and may not be distributed. -** You may use this file in accordance with the terms and conditions -** contained in the Technology Preview License Agreement accompanying -** this package. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 2.1 as published by the Free Software -** Foundation and appearing in the file LICENSE.LGPL included in the -** packaging of this file. Please review the following information to -** ensure the GNU Lesser General Public License version 2.1 requirements -** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. -** -** In addition, as a special exception, Nokia gives you certain additional -** rights. These rights are described in the Nokia Qt LGPL Exception -** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. -** -** If you have questions regarding the use of this file, please contact -** Nokia at qt-info@nokia.com. -** -** -** -** -** -** -** -** -** $QT_END_LICENSE$ -** -****************************************************************************/ - -#include "qwebsocketserver.h" - -#include -#include -#include -#include - -#include - -namespace { -template -inline static void appendBytes(QByteArray& data, T value) -{ - data.append(reinterpret_cast(&value), sizeof(value)); -} - -inline static void unmask(QByteArray& data, char mask[4]) -{ - for (int i = 0; i < data.size(); ++i) { - int j = i % 4; - data[i] = data[i] ^ mask[j]; - } -} - -inline static char bitMask(int bit) -{ - return 1 << bit; -} - -// see: http://tools.ietf.org/html/rfc6455#page-28 -static const char FIN_BIT = bitMask(7); -static const char MASKED_BIT = bitMask(7); -static const char OPCODE_RANGE = bitMask(4) - 1; -static const char PAYLOAD_RANGE = bitMask(7) - 1; -static const char EXTENDED_PAYLOAD = 126; -static const char EXTENDED_LONG_PAYLOAD = 127; -} - -QWebSocketServer::QWebSocketServer(QObject* parent) -: QObject(parent) -, m_server(new QTcpServer(this)) -{ - connect(m_server, SIGNAL(newConnection()), - SLOT(newConnection())); - connect(m_server, SIGNAL(acceptError(QAbstractSocket::SocketError)), - SIGNAL(error(QAbstractSocket::SocketError))); -} - -QWebSocketServer::~QWebSocketServer() -{ - close(); -} - -bool QWebSocketServer::listen(const QHostAddress& address, quint16 port) -{ - return m_server->listen(address, port); -} - -void QWebSocketServer::close() -{ - sendFrame(Frame::ConnectionClose, QByteArray()); - m_server->close(); -} - -quint16 QWebSocketServer::port() const -{ - return m_server->serverPort(); -} - -QHostAddress QWebSocketServer::address() const -{ - return m_server->serverAddress(); -} - -QString QWebSocketServer::errorString() const -{ - return m_server->errorString(); -} - -void QWebSocketServer::newConnection() -{ - if (!m_server->hasPendingConnections()) - return; - - QTcpSocket* connection = m_server->nextPendingConnection(); - m_connections.insert(connection, Connection()); - connect(connection, SIGNAL(readyRead()), - SLOT(readSocketData())); - connect(connection, SIGNAL(error(QAbstractSocket::SocketError)), - SIGNAL(error(QAbstractSocket::SocketError))); - connect(connection, SIGNAL(disconnected()), - SLOT(disconnected())); -} - -void QWebSocketServer::disconnected() -{ - QTcpSocket* socket = qobject_cast(sender()); - Q_ASSERT(socket); - - m_connections.remove(socket); -} - -static const QByteArray headerSwitchProtocols = QByteArrayLiteral("HTTP/1.1 101 Switching Protocols"); -static const QByteArray headerGet = QByteArrayLiteral("GET "); -static const QByteArray headerHTTP = QByteArrayLiteral("HTTP/1.1"); -static const QByteArray headerHost = QByteArrayLiteral("Host: "); -static const QByteArray headerUpgrade = QByteArrayLiteral("Upgrade: websocket"); -static const QByteArray headerConnection = QByteArrayLiteral("Connection: Upgrade"); -static const QByteArray headerSecKey = QByteArrayLiteral("Sec-WebSocket-Key: "); -static const QByteArray headerSecProtocol = QByteArrayLiteral("Sec-WebSocket-Protocol: "); -static const QByteArray headerSecVersion = QByteArrayLiteral("Sec-WebSocket-Version: 13"); -static const QByteArray headerSecAccept = QByteArrayLiteral("Sec-WebSocket-Accept: "); -static const QByteArray headerOrigin = QByteArrayLiteral("Origin: "); -static const QByteArray headerMagicKey = QByteArrayLiteral("258EAFA5-E914-47DA-95CA-C5AB0DC85B11"); -static const QByteArray headerEOL = QByteArrayLiteral("\r\n"); -static const QByteArray httpBadRequest = QByteArrayLiteral("HTTP/1.1 400 Bad Request\r\n"); - -void QWebSocketServer::readSocketData() -{ - QTcpSocket* socket = qobject_cast(sender()); - Q_ASSERT(socket); - - Connection& connection = m_connections[socket]; - - if (!connection.header.wasUpgraded) { - readHeaderData(socket, connection.header); - } - - if (connection.header.wasUpgraded) { - while (socket->bytesAvailable()) { - if (!readFrameData(socket, connection.currentFrame)) { - close(socket, connection.header); - } - } - } -} - -void QWebSocketServer::readHeaderData(QTcpSocket* socket, HeaderData& header) -{ - while (socket->canReadLine()) { - QByteArray line = socket->readLine().trimmed(); - if (line.isEmpty()) { - // finalize - if (isValid(header)) { - upgrade(socket, header); - } else { - close(socket, header); - } - break; - } else if (line.startsWith(headerGet) && line.endsWith(headerHTTP)) { - header.path = line.mid(headerGet.size(), line.size() - headerGet.size() - headerHTTP.size()).trimmed(); - } else if (line.startsWith(headerHost)) { - header.host = line.mid(headerHost.size()).trimmed(); - } else if (line.startsWith(headerSecKey)) { - header.key = line.mid(headerSecKey.size()).trimmed(); - } else if (line.startsWith(headerOrigin)) { - header.origin = line.mid(headerOrigin.size()).trimmed(); - } else if (line.startsWith(headerSecProtocol)) { - header.protocol = line.mid(headerSecProtocol.size()).trimmed(); - } else if (line == headerUpgrade) { - header.hasUpgrade = true; - } else if (line == headerConnection) { - header.hasConnection = true; - } else if (line == headerSecVersion) { - header.hasVersion = true; - } else { - header.otherHeaders << line; - } - } -} - -// see: http://tools.ietf.org/html/rfc6455#page-28 -bool QWebSocketServer::readFrameData(QTcpSocket* socket, Frame& frame) -{ - int bytesAvailable = socket->bytesAvailable(); - if (frame.state == Frame::ReadStart) { - if (bytesAvailable < 2) { - return true; - } - uchar buffer[2]; - socket->read(reinterpret_cast(buffer), 2); - bytesAvailable -= 2; - frame.fin = buffer[0] & FIN_BIT; - // skip rsv1, rsv2, rsv3 - // last four bits are the opcode - quint8 opcode = buffer[0] & OPCODE_RANGE; - if (opcode != Frame::ContinuationFrame && opcode != Frame::BinaryFrame && - opcode != Frame::ConnectionClose && opcode != Frame::TextFrame && - opcode != Frame::Ping && opcode != Frame::Pong) - { - qWarning() << "invalid opcode: " << opcode; - return false; - } - frame.opcode = static_cast(opcode); - // test first, i.e. highest bit for mask - frame.masked = buffer[1] & MASKED_BIT; - if (!frame.masked) { - qWarning() << "unmasked frame received"; - return false; - } - // final seven bits are the payload length - frame.length = static_cast(buffer[1] & PAYLOAD_RANGE); - if (frame.length == EXTENDED_PAYLOAD) { - frame.state = Frame::ReadExtendedPayload; - } else if (frame.length == EXTENDED_LONG_PAYLOAD) { - frame.state = Frame::ReadExtendedLongPayload; - } else { - frame.state = Frame::ReadMask; - } - } - if (frame.state == Frame::ReadExtendedPayload) { - if (bytesAvailable < 2) { - return true; - } - uchar buffer[2]; - socket->read(reinterpret_cast(buffer), 2); - bytesAvailable -= 2; - frame.length = qFromBigEndian(buffer); - frame.state = Frame::ReadMask; - } - if (frame.state == Frame::ReadExtendedLongPayload) { - if (bytesAvailable < 8) { - return true; - } - uchar buffer[8]; - socket->read(reinterpret_cast(buffer), 8); - bytesAvailable -= 8; - quint64 longSize = qFromBigEndian(buffer); - // QByteArray uses int for size type so limit ourselves to that size as well - if (longSize > static_cast(std::numeric_limits::max())) { - return false; - } - frame.length = static_cast(longSize); - frame.state = Frame::ReadMask; - } - if (frame.state == Frame::ReadMask) { - if (bytesAvailable < 4) { - return true; - } - socket->read(frame.mask, 4); - bytesAvailable -= 4; - frame.state = Frame::ReadData; - frame.data.reserve(frame.length); - } - if (frame.state == Frame::ReadData && (bytesAvailable || !frame.length)) { - frame.data.append(socket->read(qMin(frame.length - frame.data.size(), bytesAvailable))); - if (frame.data.size() == frame.length) { - frame.state = Frame::ReadStart; - handleFrame(socket, frame); - } - } - return true; -} - -void QWebSocketServer::handleFrame(QTcpSocket* socket, Frame& frame) -{ - unmask(frame.data, frame.mask); - - // fragmentation support - see http://tools.ietf.org/html/rfc6455#page-33 - if (!frame.fin) { - if (frame.opcode != Frame::ContinuationFrame) { - frame.initialOpcode = frame.opcode; - } - frame.fragments += frame.data; - } else if (frame.fin && frame.opcode == Frame::ContinuationFrame) { - frame.opcode = frame.initialOpcode; - frame.data = frame.fragments + frame.data; - } // otherwise if it's fin and a non-continuation frame its a single-frame message - - switch (frame.opcode) { - case Frame::ContinuationFrame: - // do nothing - break; - case Frame::Ping: - socket->write(frameHeader(Frame::Pong, 0)); - break; - case Frame::Pong: - emit pongReceived(); - break; - case Frame::ConnectionClose: - ///TODO: handle? - qWarning("Unhandled connection close frame"); - break; - case Frame::BinaryFrame: - emit binaryDataReceived(frame.data); - break; - case Frame::TextFrame: - emit textDataReceived(QString::fromUtf8(frame.data)); - break; - } - - if (frame.fin) { - frame = Frame(); - } -} - -bool QWebSocketServer::isValid(const HeaderData& header) -{ - return !header.path.isEmpty() && !header.host.isEmpty() && !header.key.isEmpty() - && header.hasUpgrade && header.hasConnection && header.hasVersion; -} - -void QWebSocketServer::close(QTcpSocket* socket, const HeaderData& header) -{ - if (header.wasUpgraded) { - //TODO: implement this properly - see http://tools.ietf.org/html/rfc6455#page-36 - socket->write(frameHeader(Frame::Frame::ConnectionClose, 0)); - } else { - socket->write(httpBadRequest); - } - socket->close(); -} - -void QWebSocketServer::upgrade(QTcpSocket* socket, HeaderData& header) -{ - socket->write(headerSwitchProtocols); - socket->write(headerEOL); - - socket->write(headerUpgrade); - socket->write(headerEOL); - - socket->write(headerConnection); - socket->write(headerEOL); - - socket->write(headerSecAccept); - socket->write(QCryptographicHash::hash( header.key + headerMagicKey, QCryptographicHash::Sha1 ).toBase64()); - socket->write(headerEOL); - - if (!header.protocol.isEmpty()) { - socket->write(headerSecProtocol); - socket->write(header.protocol); - socket->write(headerEOL); - } - - socket->write(headerEOL); - - header.wasUpgraded = true; -} - -void QWebSocketServer::sendMessage(const QByteArray& message) const -{ - sendFrame(Frame::TextFrame, message); -} - -void QWebSocketServer::sendFrame(Frame::Opcode opcode, const QByteArray& data) const -{ - const QByteArray& header = frameHeader(opcode, data.size()); - QHash< QTcpSocket*, Connection >::const_iterator it = m_connections.constBegin(); - while (it != m_connections.constEnd()) { - if (it.value().header.wasUpgraded) { - it.key()->write(header); - it.key()->write(data); - } - ++it; - } -} - -// see: http://tools.ietf.org/html/rfc6455#page-28 -QByteArray QWebSocketServer::frameHeader(QWebSocketServer::Frame::Opcode opcode, const int dataSize) const -{ - // we only support single frames for now - Q_ASSERT(opcode != Frame::ContinuationFrame); - - QByteArray header; - header.reserve(4); - header.append(FIN_BIT | opcode); - if (dataSize < EXTENDED_PAYLOAD) { - header.append(static_cast(dataSize)); - } else if (dataSize < std::numeric_limits::max()) { - header.append(EXTENDED_PAYLOAD); - appendBytes(header, qToBigEndian(dataSize)); - } else { - header.append(EXTENDED_LONG_PAYLOAD); - appendBytes(header, qToBigEndian(dataSize)); - } - return header; -} - -void QWebSocketServer::ping() const -{ - sendFrame(Frame::Ping, QByteArray()); -} diff --git a/src/qwebsocketserver.h b/src/qwebsocketserver.h deleted file mode 100644 index 3a57d2e..0000000 --- a/src/qwebsocketserver.h +++ /dev/null @@ -1,158 +0,0 @@ -/**************************************************************************** -** -** Copyright (C) 2013 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff -** Contact: http://www.qt-project.org/legal -** -** This file is part of the QWebChannel module on Qt labs. -** -** $QT_BEGIN_LICENSE:LGPL$ -** No Commercial Usage -** This file contains pre-release code and may not be distributed. -** You may use this file in accordance with the terms and conditions -** contained in the Technology Preview License Agreement accompanying -** this package. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 2.1 as published by the Free Software -** Foundation and appearing in the file LICENSE.LGPL included in the -** packaging of this file. Please review the following information to -** ensure the GNU Lesser General Public License version 2.1 requirements -** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. -** -** In addition, as a special exception, Nokia gives you certain additional -** rights. These rights are described in the Nokia Qt LGPL Exception -** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. -** -** If you have questions regarding the use of this file, please contact -** Nokia at qt-info@nokia.com. -** -** -** -** -** -** -** -** -** $QT_END_LICENSE$ -** -****************************************************************************/ - -#ifndef QWEBSOCKET_H -#define QWEBSOCKET_H - -#include -#include - -class QTcpServer; -class QTcpSocket; - -class QWebSocketServer : public QObject -{ - Q_OBJECT - -public: - explicit QWebSocketServer(QObject* parent = 0); - virtual ~QWebSocketServer(); - - bool listen(const QHostAddress& address = QHostAddress::LocalHost, quint16 port = 0); - void close(); - - QHostAddress address() const; - quint16 port() const; - - QString errorString() const; - -signals: - void opened(); - void error(QAbstractSocket::SocketError); - void textDataReceived(const QString& data); - void binaryDataReceived(const QByteArray& data); - void pongReceived(); - -public slots: - void sendMessage(const QByteArray& message) const; - void ping() const; - -private slots: - void newConnection(); - void readSocketData(); - void disconnected(); - -protected: - struct HeaderData - { - HeaderData() - : hasVersion(false) - , hasUpgrade(false) - , hasConnection(false) - , wasUpgraded(false) - { - } - QByteArray path; - QByteArray host; - QByteArray origin; - QByteArray key; - QByteArray protocol; - QVector otherHeaders; - // no bitmap here - we only have few of these objects - bool hasVersion; - bool hasUpgrade; - bool hasConnection; - bool wasUpgraded; - }; - virtual bool isValid(const HeaderData& connection); - -private: - struct Frame - { - enum State { - ReadStart, - ReadExtendedPayload, - ReadExtendedLongPayload, - ReadMask, - ReadData, - Finished - }; - enum Opcode { - ContinuationFrame = 0x0, - TextFrame = 0x1, - BinaryFrame = 0x2, - ConnectionClose = 0x8, - Ping = 0x9, - Pong = 0xA - }; - // no bitmap here - we only have a few of these objects - State state; - Opcode opcode; - bool fin; - bool masked; - ///NOTE: standard says unsigned 64bit integer but QByteArray only supports 'int' size - int length; - char mask[4]; - QByteArray data; - // fragmentation support - Opcode initialOpcode; - QByteArray fragments; - }; - struct Connection - { - HeaderData header; - Frame currentFrame; - }; - - void readHeaderData(QTcpSocket* socket, HeaderData& header); - void close(QTcpSocket* socket, const HeaderData& header); - void upgrade(QTcpSocket* socket, HeaderData& header); - bool readFrameData(QTcpSocket* socket, Frame& frame); - void handleFrame(QTcpSocket* socket, Frame& frame); - - void sendFrame(Frame::Opcode opcode, const QByteArray& data) const; - void sendFrame(QTcpSocket* socket, Frame::Opcode opcode, const QByteArray& data) const; - QByteArray frameHeader(Frame::Opcode opcode, const int dataSize) const; - - QTcpServer* m_server; - QHash m_connections; -}; - -#endif // QWEBSOCKET_H diff --git a/src/resources.qrc b/src/resources.qrc deleted file mode 100644 index 821e911..0000000 --- a/src/resources.qrc +++ /dev/null @@ -1,6 +0,0 @@ - - - webchannel.js - qobject.js - - diff --git a/src/signalhandler.h b/src/signalhandler.h deleted file mode 100644 index 0db6085..0000000 --- a/src/signalhandler.h +++ /dev/null @@ -1,278 +0,0 @@ -/**************************************************************************** -** -** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). -** Copyright (C) 2013 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff -** Contact: http://www.qt-project.org/legal -* -** This file is part of the QtCore 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 Digia. For licensing terms and -** conditions see http://qt.digia.com/licensing. For further information -** use the contact form at http://qt.digia.com/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 2.1 as published by the Free Software -** Foundation and appearing in the file LICENSE.LGPL included in the -** packaging of this file. Please review the following information to -** ensure the GNU Lesser General Public License version 2.1 requirements -** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. -** -** In addition, as a special exception, Digia gives you certain additional -** rights. These rights are described in the Digia Qt LGPL Exception -** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 3.0 as published by the Free Software -** Foundation and appearing in the file LICENSE.GPL included in the -** packaging of this file. Please review the following information to -** ensure the GNU General Public License version 3.0 requirements will be -** met: http://www.gnu.org/copyleft/gpl.html. -** -** -** $QT_END_LICENSE$ -** -****************************************************************************/ - -#ifndef SIGNALHANDLER_H -#define SIGNALHANDLER_H - -#include -#include -#include -#include -#include - -/** - * The signal handler is similar to QSignalSpy, but geared towards the usecase of the web channel. - * - * It allows connecting to any number of signals of arbitrary objects and forwards the signal - * invocations to the Receiver by calling its signalEmitted function, which takes the object, - * signal index and a QVariantList of arguments. - */ -template -class SignalHandler : public QObject -{ -public: - SignalHandler(Receiver *receiver, QObject *parent = 0) - : QObject(parent) - , m_receiver(receiver) - { - } - - /** - * Connect to a signal of @p object identified by @p signalIndex. - * - * If the handler is already connected to the signal, an internal counter is increased, - * i.e. the handler never connects multiple times to the same signal. - */ - void connectTo(const QObject *object, const int signalIndex); - - /** - * Decrease the connection counter for the connection to the given signal. - * - * When the counter drops to zero, the connection is disconnected. - */ - void disconnectFrom(const QObject *object, const int signalIndex); - - /** - * @internal - * - * Custom implementation of qt_metacall which calls dispatch() for connected signals. - */ - int qt_metacall(QMetaObject::Call call, int methodId, void **args) Q_DECL_OVERRIDE; - - /** - * Reset all connections, useful for benchmarks. - */ - void clear(); - -private: - /** - * Exctract the arguments of a signal call and pass them to the receiver. - * - * The @p argumentData is converted to a QVariantList and then passed to the receiver's - * signalEmitted method. - */ - void dispatch(const QObject *object, const int signalIdx, void **argumentData); - - Receiver *m_receiver; - - // maps meta object -> signalIndex -> list of arguments - // NOTE: This data is "leaked" on disconnect until deletion of the handler, is this a problem? - typedef QVector ArgumentTypeList; - typedef QHash SignalArgumentHash; - QHash m_signalArgumentTypes; - - /* - * Tracks how many connections are active to object signals. - * - * Maps object -> signalIndex -> pair of connection and number of connections - * - * Note that the handler is connected to the signal only once, whereas clients - * may have connected multiple times. - * - * TODO: Move more of this logic to the HTML client side, esp. the connection counting. - */ - typedef QPair ConnectionPair; - typedef QHash SignalConnectionHash; - typedef QHash ConnectionHash; - ConnectionHash m_connectionsCounter; -}; - -/** - * Find and return the signal of index @p signalIndex in the meta object of @p object and return it. - * - * The return value is also verified to ensure it is a signal. - */ -QMetaMethod findSignal(const QMetaObject *metaObject, const int signalIndex) -{ - QMetaMethod signal = metaObject->method(signalIndex); - if (!signal.isValid()) { - qWarning("Cannot find signal with index %d of object %s", signalIndex, metaObject->className()); - return QMetaMethod(); - } - Q_ASSERT(signal.methodType() == QMetaMethod::Signal); - return signal; -} - -template -void SignalHandler::connectTo(const QObject *object, const int signalIndex) -{ - const QMetaObject *metaObject = object->metaObject(); - const QMetaMethod &signal = findSignal(metaObject, signalIndex); - if (!signal.isValid()) { - return; - } - - ConnectionPair &connectionCounter = m_connectionsCounter[object][signalIndex]; - if (connectionCounter.first) { - // increase connection counter if already connected - ++connectionCounter.second; - return; - } // otherwise not yet connected, do so now - - const int memberOffset = QObject::staticMetaObject.methodCount(); - QMetaObject::Connection connection = QMetaObject::connect(object, signal.methodIndex(), this, memberOffset, Qt::DirectConnection, 0); - if (!connection) { - qWarning() << "SignalHandler: QMetaObject::connect returned false. Unable to connect to" << object << signal.name() << signal.methodSignature(); - return; - } - connectionCounter.first = connection; - connectionCounter.second = 1; - - if (!m_signalArgumentTypes.value(metaObject).contains(signal.methodIndex())) { - // find the type ids of the signal parameters, see also QSignalSpy::initArgs - QVector args; - args.reserve(signal.parameterCount()); - for (int i = 0; i < signal.parameterCount(); ++i) { - int tp = signal.parameterType(i); - if (tp == QMetaType::UnknownType && object) { - void *argv[] = { &tp, &i }; - QMetaObject::metacall(const_cast(object), - QMetaObject::RegisterMethodArgumentMetaType, - signal.methodIndex(), argv); - if (tp == -1) { - tp = QMetaType::UnknownType; - } - } - if (tp == QMetaType::UnknownType) { - Q_ASSERT(tp != QMetaType::Void); // void parameter => metaobject is corrupt - qWarning("Don't know how to handle '%s', use qRegisterMetaType to register it.", - signal.parameterNames().at(i).constData()); - } - args << tp; - } - - m_signalArgumentTypes[metaObject][signal.methodIndex()] = args; - } -} - -template -void SignalHandler::dispatch(const QObject *object, const int signalIdx, void **argumentData) -{ - Q_ASSERT(m_signalArgumentTypes.contains(object->metaObject())); - const QHash > &objectSignalArgumentTypes = m_signalArgumentTypes.value(object->metaObject()); - QHash >::const_iterator signalIt = objectSignalArgumentTypes.constFind(signalIdx); - if (signalIt == objectSignalArgumentTypes.constEnd()) { - // not connected to this signal, skip - return; - } - const QVector &argumentTypes = *signalIt; - QVariantList arguments; - arguments.reserve(argumentTypes.count()); - // TODO: basic overload resolution based on number of arguments? - for (int i = 0; i < argumentTypes.count(); ++i) { - const QMetaType::Type type = static_cast(argumentTypes.at(i)); - QVariant arg; - if (type == QMetaType::QVariant) { - arg = *reinterpret_cast(argumentData[i + 1]); - } else { - arg = QVariant(type, argumentData[i + 1]); - } - arguments.append(arg); - } - m_receiver->signalEmitted(object, signalIdx, arguments); -} - -template -void SignalHandler::disconnectFrom(const QObject *object, const int signalIndex) -{ - Q_ASSERT(m_connectionsCounter.value(object).contains(signalIndex)); - ConnectionPair &connection = m_connectionsCounter[object][signalIndex]; - --connection.second; - if (!connection.second || !connection.first) { - QObject::disconnect(connection.first); - m_connectionsCounter[object].remove(signalIndex); - if (m_connectionsCounter[object].isEmpty()) { - m_connectionsCounter.remove(object); - } - } -} - -template -int SignalHandler::qt_metacall(QMetaObject::Call call, int methodId, void **args) -{ - methodId = QObject::qt_metacall(call, methodId, args); - if (methodId < 0) - return methodId; - - if (call == QMetaObject::InvokeMetaMethod) { - if (methodId == 0) { - Q_ASSERT(sender()); - Q_ASSERT(senderSignalIndex() != -1); - dispatch(sender(), senderSignalIndex(), args); - static const int destroyedIndex = metaObject()->indexOfSignal("destroyed(QObject*)"); - if (senderSignalIndex() == destroyedIndex) { - ConnectionHash::iterator it = m_connectionsCounter.find(sender()); - Q_ASSERT(it != m_connectionsCounter.end()); - foreach (const ConnectionPair &connection, *it) { - QObject::disconnect(connection.first); - } - m_connectionsCounter.erase(it); - } - } - --methodId; - } - return methodId; -} - -template -void SignalHandler::clear() -{ - foreach (const SignalConnectionHash &connections, m_connectionsCounter) { - foreach (const ConnectionPair &connection, connections) { - QObject::disconnect(connection.first); - } - } - m_connectionsCounter.clear(); - m_signalArgumentTypes.clear(); -} - -#endif // SIGNALHANDLER_H diff --git a/src/src.pri b/src/src.pri deleted file mode 100644 index 5ae01f3..0000000 --- a/src/src.pri +++ /dev/null @@ -1,11 +0,0 @@ -QT += network -SOURCES += \ - $$PWD/qwebchannel.cpp \ - $$PWD/qtmetaobjectpublisher.cpp \ - $$PWD/qwebsocketserver.cpp -HEADERS += \ - $$PWD/qwebchannel.h \ - $$PWD/qtmetaobjectpublisher.h \ - $$PWD/qwebsocketserver.h \ - $$PWD/variantargument.h \ - $$PWD/signalhandler.h diff --git a/src/src.pro b/src/src.pro index bd2e3c7..900a924 100644 --- a/src/src.pro +++ b/src/src.pro @@ -1,40 +1,8 @@ -include(src.pri) +TEMPLATE = subdirs -TEMPLATE = lib -TARGET = qwebchannel -TARGETPATH = Qt/labs/WebChannel -QT += qml quick -CONFIG += qt plugin +SUBDIRS += webchannel -TARGET = $$qtLibraryTarget($$TARGET) - -# Input -SOURCES += $$PWD/qwebchannel_plugin.cpp -HEADERS += $$PWD/qwebchannel_plugin.h - -RESOURCES += \ - resources.qrc - -OTHER_FILES = qmldir \ - webchannel.js \ - qobject.js - -target.path = $$[QT_INSTALL_QML]/$$TARGETPATH - -# extra files that need to be deployed to $$TARGETPATH -DEPLOY_FILES = \ - qmldir - -for(FILE, DEPLOY_FILES): qmldir.files += $$PWD/$$FILE -qmldir.path += $$[QT_INSTALL_QML]/$$TARGETPATH - -INSTALLS += target qmldir - -# ensure that plugin is put into the correct folder structure -DESTDIR = $$TARGETPATH - -# copy files in order to run tests without calling make install first -for(FILE, DEPLOY_FILES) { - PRE_TARGETDEPS += $$PWD/$$FILE - QMAKE_POST_LINK += $$quote($$QMAKE_COPY \"$$PWD/$$FILE\" $$OUT_PWD/$$TARGETPATH$$escape_expand(\n\t)) +qtHaveModule(quick) { + SUBDIRS += imports + imports.depends = webchannel } diff --git a/src/variantargument.h b/src/variantargument.h deleted file mode 100644 index d5382bd..0000000 --- a/src/variantargument.h +++ /dev/null @@ -1,103 +0,0 @@ -/**************************************************************************** -** -** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). -** Copyright (C) 2013 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff -** Contact: http://www.qt-project.org/legal -* -** This file is part of the QtCore 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 Digia. For licensing terms and -** conditions see http://qt.digia.com/licensing. For further information -** use the contact form at http://qt.digia.com/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 2.1 as published by the Free Software -** Foundation and appearing in the file LICENSE.LGPL included in the -** packaging of this file. Please review the following information to -** ensure the GNU Lesser General Public License version 2.1 requirements -** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. -** -** In addition, as a special exception, Digia gives you certain additional -** rights. These rights are described in the Digia Qt LGPL Exception -** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 3.0 as published by the Free Software -** Foundation and appearing in the file LICENSE.GPL included in the -** packaging of this file. Please review the following information to -** ensure the GNU General Public License version 3.0 requirements will be -** met: http://www.gnu.org/copyleft/gpl.html. -** -** -** $QT_END_LICENSE$ -** -****************************************************************************/ - -#ifndef VARIANTARGUMENT_H -#define VARIANTARGUMENT_H - -#include -#include - -/** - * RAII QVariant to Q[Generic]Argument conversion - */ -class VariantArgument -{ -public: - explicit VariantArgument() - : m_data(0) - , m_paramType(0) - { - } - - /// TODO: test with C++ methods that don't take a QVariant as arg - /// also test conversions - void setValue(const QVariant &value, int paramType) - { - if (m_data) { - QMetaType::destroy(m_paramType, m_data); - m_name.clear(); - m_data = 0; - } - - m_paramType = paramType; - - if (value.isValid()) { - m_name = value.typeName(); - m_data = QMetaType::create(m_paramType, value.constData()); - } - } - - ~VariantArgument() - { - if (m_data) { - QMetaType::destroy(m_paramType, m_data); - m_data = 0; - } - } - - operator QGenericArgument() const - { - if (!m_data) { - return QGenericArgument(); - } - return QGenericArgument(m_name.constData(), m_data); - } - -private: - Q_DISABLE_COPY(VariantArgument) - - QByteArray m_name; - void* m_data; - int m_paramType; -}; - -#endif // VARIANTARGUMENT_H diff --git a/src/webchannel.js b/src/webchannel.js deleted file mode 100644 index f29bafc..0000000 --- a/src/webchannel.js +++ /dev/null @@ -1,131 +0,0 @@ -/**************************************************************************** -** -** Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies). -** All rights reserved. -** Contact: Nokia Corporation (qt-info@nokia.com) -** -** Copyright (C) 2013 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff -** Contact: http://www.qt-project.org/legal -** -** This file is part of the QWebChannel module on Qt labs. -** -** $QT_BEGIN_LICENSE:LGPL$ -** No Commercial Usage -** This file contains pre-release code and may not be distributed. -** You may use this file in accordance with the terms and conditions -** contained in the Technology Preview License Agreement accompanying -** this package. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 2.1 as published by the Free Software -** Foundation and appearing in the file LICENSE.LGPL included in the -** packaging of this file. Please review the following information to -** ensure the GNU Lesser General Public License version 2.1 requirements -** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. -** -** In addition, as a special exception, Nokia gives you certain additional -** rights. These rights are described in the Nokia Qt LGPL Exception -** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. -** -** If you have questions regarding the use of this file, please contact -** Nokia at qt-info@nokia.com. -** -** -** -** -** -** -** -** -** $QT_END_LICENSE$ -** -****************************************************************************/ - -"use strict"; - -var QWebChannel = function(baseUrl, initCallback) -{ - var channel = this; - // support multiple channels listening to the same socket - // the responses to channel.exec must be distinguishable - // see: http://stackoverflow.com/a/2117523/35250 - this.id = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { - var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8); - return v.toString(16); - }); - ///TODO: use ssl? - var socketUrl = "ws://" + baseUrl; - this.socket = new WebSocket(socketUrl, "QWebChannel"); - this.send = function(data) - { - if (typeof(data) !== "string") { - data = JSON.stringify(data); - } - channel.socket.send(data); - }; - - this.socket.onopen = function() - { - initCallback(channel); - }; - this.socket.onclose = function() - { - console.error("web channel closed"); - }; - this.socket.onerror = function(error) - { - console.error("web channel error: " + error); - }; - this.socket.onmessage = function(message) - { - var jsonData = JSON.parse(message.data); - if (jsonData.id === undefined) { - console.error("invalid message received:", message.data); - return; - } - if (jsonData.data === undefined) { - jsonData.data = {}; - } - if (jsonData.response) { - if (jsonData.id[0] === channel.id) { - channel.execCallbacks[jsonData.id[1]](jsonData.data); - delete channel.execCallbacks[jsonData.id]; - } - } else if (channel.subscriptions[jsonData.id]) { - channel.subscriptions[jsonData.id].forEach(function(callback) { - (callback)(jsonData.data); } - ); - } - }; - - this.subscriptions = {}; - this.subscribe = function(id, callback) - { - if (channel.subscriptions[id]) { - channel.subscriptions[id].push(callback); - } else { - channel.subscriptions[id] = [callback]; - } - }; - - this.execCallbacks = {}; - this.execId = 0; - this.exec = function(data, callback) - { - if (!callback) { - // if no callback is given, send directly - channel.send({data: data}); - return; - } - if (channel.execId === Number.MAX_VALUE) { - // wrap - channel.execId = Number.MIN_VALUE; - } - var id = channel.execId++; - channel.execCallbacks[id] = callback; - channel.send({"id": [channel.id, id], "data": data}); - }; - - this.objectMap = {}; -}; diff --git a/src/webchannel/qmetaobjectpublisher.cpp b/src/webchannel/qmetaobjectpublisher.cpp new file mode 100644 index 0000000..c141fb2 --- /dev/null +++ b/src/webchannel/qmetaobjectpublisher.cpp @@ -0,0 +1,730 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Copyright (C) 2013 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff +** Contact: http://www.qt-project.org/legal +* +** This file is part of the QtWebChannel 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 Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/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 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qmetaobjectpublisher.h" +#include "qwebchannel.h" + +#include "variantargument_p.h" +#include "signalhandler_p.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { +const QString KEY_SIGNALS = QStringLiteral("signals"); +const QString KEY_METHODS = QStringLiteral("methods"); +const QString KEY_PROPERTIES = QStringLiteral("properties"); +const QString KEY_ENUMS = QStringLiteral("enums"); +const QString KEY_QOBJECT = QStringLiteral("__QObject*__"); +const QString KEY_ID = QStringLiteral("id"); +const QString KEY_DATA = QStringLiteral("data"); +const QString KEY_OBJECT = QStringLiteral("object"); +const QString KEY_DESTROYED = QStringLiteral("destroyed"); +const QString KEY_SIGNAL = QStringLiteral("signal"); +const QString KEY_TYPE = QStringLiteral("type"); +const QString KEY_MESSAGE = QStringLiteral("message"); +const QString KEY_METHOD = QStringLiteral("method"); +const QString KEY_ARGS = QStringLiteral("args"); +const QString KEY_PROPERTY = QStringLiteral("property"); +const QString KEY_VALUE = QStringLiteral("value"); + +const QString TYPE_SIGNAL = QStringLiteral("Qt.signal"); +const QString TYPE_PROPERTY_UPDATE = QStringLiteral("Qt.propertyUpdate"); +const QString TYPE_INIT = QStringLiteral("Qt.init"); +const QString TYPE_IDLE = QStringLiteral("Qt.idle"); +const QString TYPE_DEBUG = QStringLiteral("Qt.debug"); +const QString TYPE_INVOKE_METHOD = QStringLiteral("Qt.invokeMethod"); +const QString TYPE_CONNECT_TO_SIGNAL = QStringLiteral("Qt.connectToSignal"); +const QString TYPE_DISCONNECT_FROM_SIGNAL = QStringLiteral("Qt.disconnectFromSignal"); +const QString TYPE_SET_PROPERTY = QStringLiteral("Qt.setProperty"); + +QString objectId(const QObject *object) +{ + return QString::number(quintptr(object), 16); +} + +const int s_destroyedSignalIndex = QObject::staticMetaObject.indexOfMethod("destroyed(QObject*)"); + +/// TODO: what is the proper value here? +const int PROPERTY_UPDATE_INTERVAL = 50; +} + +struct QMetaObjectPublisherPrivate +{ + QMetaObjectPublisherPrivate(QMetaObjectPublisher *q) + : q(q) + , signalHandler(this) + , clientIsIdle(false) + , blockUpdates(false) + , pendingInit(false) + , propertyUpdatesInitialized(false) + { + } + + /** + * Set the client to idle or busy, based on the value of @p isIdle. + * + * When the value changed, start/stop the property update timer accordingly. + */ + void setClientIsIdle(bool isIdle); + + /** + * Initialize clients by sending them the class information of the registered objects. + * + * Furthermore, if that was not done already, connect to their property notify signals. + */ + void initializeClients(); + + /** + * Go through all properties of the given object and connect to their notify signal. + * + * When receiving a notify signal, it will store the information in pendingPropertyUpdates which + * gets send via a Qt.propertyUpdate message to the server when the grouping timer timeouts. + */ + void initializePropertyUpdates(const QObject *const object, const QVariantMap &objectInfo); + + /** + * Send the clients the new property values since the last time this function was invoked. + * + * This is a grouped batch of all properties for which their notify signal was emitted. + * The list of signals as well as the arguments they contained, are also transmitted to + * the remote clients. + * + * @sa timer, initializePropertyUpdates + */ + void sendPendingPropertyUpdates(); + + /** + * Invoke the method of index @p methodIndex on @p object with the arguments @p args. + * + * The return value of the method invocation is then transmitted to the calling client + * via a webchannel response to the message identified by @p id. + */ + bool invokeMethod(QObject *const object, const int methodIndex, const QJsonArray &args, const QJsonValue &id); + + /** + * Callback of the signalHandler which forwards the signal invocation to the webchannel clients. + */ + void signalEmitted(const QObject *object, const int signalIndex, const QVariantList &arguments); + + /** + * Callback for registered or wrapped objects which erases all data related to @p object. + * + * @sa signalEmitted + */ + void objectDestroyed(const QObject *object); + + /** + * Given a QVariant containing a QObject*, wrap the object and register for property updates + * return the objects class information. + * + * All other input types are returned as-is. + * + * TODO: support wrapping of initially-registered objects + */ + QVariant wrapResult(const QVariant &result); + + /** + * Invoke delete later on @p object. + */ + void deleteWrappedObject(QObject *object) const; + + QMetaObjectPublisher *q; + QPointer webChannel; + SignalHandler signalHandler; + + // true when the client is idle, false otherwise + bool clientIsIdle; + + // true when no property updates should be sent, false otherwise + bool blockUpdates; + + // true when at least one client needs to be initialized, + // i.e. when a Qt.init came in which was not handled yet. + bool pendingInit; + + // true when at least one client was initialized and thus + // the property updates have been initialized and the + // object info map set. + bool propertyUpdatesInitialized; + + // Map of registered objects indexed by their id. + QHash registeredObjects; + + // Map the registered objects to their id. + QHash registeredObjectIds; + + // Map of object names to maps of signal indices to a set of all their properties. + // The last value is a set as a signal can be the notify signal of multiple properties. + typedef QHash > SignalToPropertyNameMap; + QHash signalToPropertyMap; + + // Objects that changed their properties and are waiting for idle client. + // map of object name to map of signal index to arguments + typedef QHash SignalToArgumentsMap; + typedef QHash PendingPropertyUpdates; + PendingPropertyUpdates pendingPropertyUpdates; + + // Maps wrapped object to class info + QHash wrappedObjects; + + // Aggregate property updates since we get multiple Qt.idle message when we have multiple + // clients. They all share the same QWebProcess though so we must take special care to + // prevent message flooding. + QBasicTimer timer; +}; + +void QMetaObjectPublisherPrivate::setClientIsIdle(bool isIdle) +{ + if (clientIsIdle == isIdle) { + return; + } + clientIsIdle = isIdle; + if (!isIdle && timer.isActive()) { + timer.stop(); + } else if (isIdle && !timer.isActive()) { + timer.start(PROPERTY_UPDATE_INTERVAL, q); + } +} + +void QMetaObjectPublisherPrivate::initializeClients() +{ + QJsonObject objectInfos; + { + const QHash::const_iterator end = registeredObjects.constEnd(); + for (QHash::const_iterator it = registeredObjects.constBegin(); it != end; ++it) { + const QVariantMap &info = q->classInfoForObject(it.value()); + if (!propertyUpdatesInitialized) { + initializePropertyUpdates(it.value(), info); + } + objectInfos[it.key()] = QJsonObject::fromVariantMap(info); + } + } + webChannel->sendMessage(TYPE_INIT, objectInfos); + propertyUpdatesInitialized = true; + pendingInit = false; +} + +void QMetaObjectPublisherPrivate::initializePropertyUpdates(const QObject *const object, const QVariantMap &objectInfo) +{ + foreach (const QVariant &propertyInfoVar, objectInfo[KEY_PROPERTIES].toList()) { + const QVariantList &propertyInfo = propertyInfoVar.toList(); + if (propertyInfo.size() < 2) { + qWarning() << "Invalid property info encountered:" << propertyInfoVar; + continue; + } + const QString &propertyName = propertyInfo.at(0).toString(); + const QVariantList &signalData = propertyInfo.at(1).toList(); + + if (signalData.isEmpty()) { + // Property without NOTIFY signal + continue; + } + + const int signalIndex = signalData.at(1).toInt(); + + QSet &connectedProperties = signalToPropertyMap[object][signalIndex]; + + // Only connect for a property update once + if (connectedProperties.isEmpty()) { + signalHandler.connectTo(object, signalIndex); + } + + connectedProperties.insert(propertyName); + } + + // also always connect to destroyed signal + signalHandler.connectTo(object, s_destroyedSignalIndex); +} + +void QMetaObjectPublisherPrivate::sendPendingPropertyUpdates() +{ + if (blockUpdates || !clientIsIdle || pendingPropertyUpdates.isEmpty()) { + return; + } + + QJsonArray data; + + // convert pending property updates to JSON data + const PendingPropertyUpdates::const_iterator end = pendingPropertyUpdates.constEnd(); + for (PendingPropertyUpdates::const_iterator it = pendingPropertyUpdates.constBegin(); it != end; ++it) { + const QObject *object = it.key(); + const QMetaObject *const metaObject = object->metaObject(); + const SignalToPropertyNameMap &objectsSignalToPropertyMap = signalToPropertyMap.value(object); + // maps property name to current property value + QJsonObject properties; + // maps signal index to list of arguments of the last emit + QJsonObject sigs; + const SignalToArgumentsMap::const_iterator sigEnd = it.value().constEnd(); + for (SignalToArgumentsMap::const_iterator sigIt = it.value().constBegin(); sigIt != sigEnd; ++sigIt) { + // TODO: use property indices + foreach (const QString &propertyName, objectsSignalToPropertyMap.value(sigIt.key())) { + int propertyIndex = metaObject->indexOfProperty(qPrintable(propertyName)); + if (propertyIndex == -1) { + qWarning("Unknown property %d encountered", propertyIndex); + continue; + } + const QMetaProperty &property = metaObject->property(propertyIndex); + properties[QString::fromLatin1(property.name())] = QJsonValue::fromVariant(property.read(object)); + } + // TODO: can we get rid of the int <-> string conversions here? + sigs[QString::number(sigIt.key())] = QJsonArray::fromVariantList(sigIt.value()); + } + QJsonObject obj; + obj[KEY_OBJECT] = registeredObjectIds.value(object); + obj[KEY_SIGNALS] = sigs; + obj[KEY_PROPERTIES] = properties; + data.push_back(obj); + } + + pendingPropertyUpdates.clear(); + webChannel->sendMessage(TYPE_PROPERTY_UPDATE, data); + setClientIsIdle(false); +} + +bool QMetaObjectPublisherPrivate::invokeMethod(QObject *const object, const int methodIndex, + const QJsonArray &args, const QJsonValue &id) +{ + const QMetaMethod &method = object->metaObject()->method(methodIndex); + + if (method.name() == QByteArrayLiteral("deleteLater")) { + // invoke `deleteLater` on wrapped QObject indirectly + deleteWrappedObject(object); + return true; + } else if (!method.isValid()) { + qWarning() << "Cannot invoke unknown method of index" << methodIndex << "on object" << object << '.'; + return false; + } else if (method.access() != QMetaMethod::Public) { + qWarning() << "Cannot invoke non-public method" << method.name() << "on object" << object << '.'; + return false; + } else if (method.methodType() != QMetaMethod::Method && method.methodType() != QMetaMethod::Slot) { + qWarning() << "Cannot invoke non-public method" << method.name() << "on object" << object << '.'; + return false; + } else if (args.size() > 10) { + qWarning() << "Cannot invoke method" << method.name() << "on object" << object << "with more than 10 arguments, as that is not supported by QMetaMethod::invoke."; + return false; + } else if (args.size() > method.parameterCount()) { + qWarning() << "Ignoring additional arguments while invoking method" << method.name() << "on object" << object << ':' + << args.size() << "arguments given, but method only takes" << method.parameterCount() << '.'; + } + + // construct converter objects of QVariant to QGenericArgument + VariantArgument arguments[10]; + for (int i = 0; i < qMin(args.size(), method.parameterCount()); ++i) { + arguments[i].setValue(args.at(i).toVariant(), method.parameterType(i)); + } + + // construct QGenericReturnArgument + QVariant returnValue; + if (method.returnType() != qMetaTypeId() && method.returnType() != qMetaTypeId()) { + // Only init variant with return type if its not a variant itself, which would + // lead to nested variants which is not what we want. + // Also, skip void-return types for obvious reasons (and to prevent a runtime warning inside Qt). + returnValue = QVariant(method.returnType(), 0); + } + QGenericReturnArgument returnArgument(method.typeName(), returnValue.data()); + + // now we can call the method + method.invoke(object, returnArgument, + arguments[0], arguments[1], arguments[2], arguments[3], arguments[4], + arguments[5], arguments[6], arguments[7], arguments[8], arguments[9]); + + // and send the return value to the client + webChannel->respond(id, QJsonValue::fromVariant(wrapResult(returnValue))); + + return true; +} + +void QMetaObjectPublisherPrivate::signalEmitted(const QObject *object, const int signalIndex, const QVariantList &arguments) +{ + if (!webChannel) { + return; + } + if (!signalToPropertyMap.value(object).contains(signalIndex)) { + QJsonObject data; + const QString &objectName = registeredObjectIds.value(object); + Q_ASSERT(!objectName.isEmpty()); + data[KEY_OBJECT] = objectName; + data[KEY_SIGNAL] = signalIndex; + if (!arguments.isEmpty()) { + // TODO: wrap (new) objects on the fly + data[KEY_ARGS] = QJsonArray::fromVariantList(arguments); + } + webChannel->sendMessage(TYPE_SIGNAL, data); + + if (signalIndex == s_destroyedSignalIndex) { + objectDestroyed(object); + } + } else { + pendingPropertyUpdates[object][signalIndex] = arguments; + if (clientIsIdle && !blockUpdates && !timer.isActive()) { + timer.start(PROPERTY_UPDATE_INTERVAL, q); + } + } +} + +void QMetaObjectPublisherPrivate::objectDestroyed(const QObject *object) +{ + const QString &id = registeredObjectIds.take(object); + Q_ASSERT(!id.isEmpty()); + bool removed = registeredObjects.remove(id); + Q_ASSERT(removed); + Q_UNUSED(removed); + + signalToPropertyMap.remove(object); + pendingPropertyUpdates.remove(object); + wrappedObjects.remove(object); +} + +QVariant QMetaObjectPublisherPrivate::wrapResult(const QVariant &result) +{ + if (QObject *object = result.value()) { + QVariantMap &objectInfo = wrappedObjects[object]; + if (!objectInfo.isEmpty()) { + // already registered, use cached information + Q_ASSERT(registeredObjectIds.contains(object)); + return objectInfo; + } // else the object is not yet wrapped, do it now + + const QString &id = objectId(object); + Q_ASSERT(!registeredObjects.contains(id)); + Q_ASSERT(!registeredObjectIds.contains(object)); + + objectInfo[KEY_QOBJECT] = true; + objectInfo[KEY_ID] = id; + objectInfo[KEY_DATA] = q->classInfoForObject(object); + + registeredObjectIds[object] = id; + registeredObjects[id] = object; + wrappedObjects.insert(object, objectInfo); + + initializePropertyUpdates(object, objectInfo); + return objectInfo; + } + + // no need to wrap this + return result; +} + +void QMetaObjectPublisherPrivate::deleteWrappedObject(QObject *object) const +{ + if (!wrappedObjects.contains(object)) { + qWarning() << "Not deleting non-wrapped object" << object; + return; + } + object->deleteLater(); +} + +QMetaObjectPublisher::QMetaObjectPublisher(QObject *parent) + : QObject(parent) + , d(new QMetaObjectPublisherPrivate(this)) +{ +} + +QMetaObjectPublisher::~QMetaObjectPublisher() +{ + +} + +QVariantMap QMetaObjectPublisher::classInfoForObjects(const QVariantMap &objectMap) const +{ + QVariantMap ret; + QMap::const_iterator it = objectMap.constBegin(); + while (it != objectMap.constEnd()) { + QObject *object = it.value().value(); + if (object) { + const QVariantMap &info = classInfoForObject(object); + if (!info.isEmpty()) { + ret[it.key()] = info; + } + } + ++it; + } + return ret; +} + +QVariantMap QMetaObjectPublisher::classInfoForObject(QObject *object) const +{ + QVariantMap data; + if (!object) { + qWarning("null object given to MetaObjectPublisher - bad API usage?"); + return data; + } + QVariantList qtSignals, qtMethods; + QVariantList qtProperties; + QVariantMap qtEnums; + const QMetaObject *metaObject = object->metaObject(); + QSet notifySignals; + QSet identifiers; + for (int i = 0; i < metaObject->propertyCount(); ++i) { + const QMetaProperty &prop = metaObject->property(i); + QVariantList propertyInfo; + const QString &propertyName = QString::fromLatin1(prop.name()); + propertyInfo.append(propertyName); + identifiers << propertyName; + if (prop.hasNotifySignal()) { + notifySignals << prop.notifySignalIndex(); + const int numParams = prop.notifySignal().parameterCount(); + if (numParams > 1) { + qWarning("Notify signal for property '%s' has %d parameters, expected zero or one.", + prop.name(), numParams); + } + // optimize: compress the common propertyChanged notification names, just send a 1 + const QByteArray ¬ifySignal = prop.notifySignal().name(); + static const QByteArray changedSuffix = QByteArrayLiteral("Changed"); + if (notifySignal.length() == changedSuffix.length() + propertyName.length() && + notifySignal.endsWith(changedSuffix) && notifySignal.startsWith(prop.name())) + { + propertyInfo.append(QVariant::fromValue(QVariantList() << 1 << prop.notifySignalIndex())); + } else { + propertyInfo.append(QVariant::fromValue(QVariantList() << QString::fromLatin1(notifySignal) << prop.notifySignalIndex())); + } + } else { + if (!prop.isConstant()) { + qWarning("Property '%s'' of object '%s' has no notify signal and is not constant, " + "value updates in HTML will be broken!", + prop.name(), object->metaObject()->className()); + } + propertyInfo.append(QVariant::fromValue(QVariantList())); + } + propertyInfo.append(prop.read(object)); + qtProperties.append(QVariant::fromValue(propertyInfo)); + } + for (int i = 0; i < metaObject->methodCount(); ++i) { + if (notifySignals.contains(i)) { + continue; + } + const QMetaMethod &method = metaObject->method(i); + //NOTE: this must be a string, otherwise it will be converted to '{}' in QML + const QString &name = QString::fromLatin1(method.name()); + // optimize: skip overloaded methods/signals or property getters, on the JS side we can only + // call one of them anyways + // TODO: basic support for overloaded signals, methods + if (identifiers.contains(name)) { + continue; + } + identifiers << name; + // send data as array to client with format: [name, index] + const QVariant data = QVariant::fromValue(QVariantList() << name << i); + if (method.methodType() == QMetaMethod::Signal) { + qtSignals.append(data); + } else if (method.access() == QMetaMethod::Public) { + qtMethods.append(data); + } + } + for (int i = 0; i < metaObject->enumeratorCount(); ++i) { + QMetaEnum enumerator = metaObject->enumerator(i); + QVariantMap values; + for (int k = 0; k < enumerator.keyCount(); ++k) { + values[QString::fromLatin1(enumerator.key(k))] = enumerator.value(k); + } + qtEnums[QString::fromLatin1(enumerator.name())] = values; + } + data[KEY_SIGNALS] = qtSignals; + data[KEY_METHODS] = qtMethods; + data[KEY_PROPERTIES] = QVariant::fromValue(qtProperties); + data[KEY_ENUMS] = qtEnums; + return data; +} + +void QMetaObjectPublisher::registerObjects(const QVariantMap &objects) +{ + if (d->propertyUpdatesInitialized) { + qWarning("Registered new object after initialization. This does not work!"); + return; + } + const QMap::const_iterator end = objects.end(); + for (QMap::const_iterator it = objects.begin(); it != end; ++it) { + QObject *object = it.value().value(); + if (!object) { + qWarning("Invalid QObject given to register under name %s", qPrintable(it.key())); + continue; + } + d->registeredObjects[it.key()] = object; + d->registeredObjectIds[object] = it.key(); + } +} + +bool QMetaObjectPublisher::handleRequest(const QJsonObject &message) +{ + if (!message.contains(KEY_DATA)) { + return false; + } + + const QJsonObject &payload = message.value(KEY_DATA).toObject(); + if (!payload.contains(KEY_TYPE)) { + return false; + } + + const QString &type = payload.value(KEY_TYPE).toString(); + if (type == TYPE_IDLE) { + d->setClientIsIdle(true); + return true; + } else if (type == TYPE_INIT) { + if (!d->blockUpdates) { + d->initializeClients(); + } else { + d->pendingInit = true; + } + return true; + } else if (type == TYPE_DEBUG) { + static QTextStream out(stdout); + out << "DEBUG: " << payload.value(KEY_MESSAGE).toString() << endl; + return true; + } else if (payload.contains(KEY_OBJECT)) { + const QString &objectName = payload.value(KEY_OBJECT).toString(); + QObject *object = d->registeredObjects.value(objectName); + if (!object) { + qWarning() << "Unknown object encountered" << objectName; + return false; + } + + if (type == TYPE_INVOKE_METHOD) { + return d->invokeMethod(object, payload.value(KEY_METHOD).toInt(-1), payload.value(KEY_ARGS).toArray(), message.value(KEY_ID)); + } else if (type == TYPE_CONNECT_TO_SIGNAL) { + d->signalHandler.connectTo(object, payload.value(KEY_SIGNAL).toInt(-1)); + return true; + } else if (type == TYPE_DISCONNECT_FROM_SIGNAL) { + d->signalHandler.disconnectFrom(object, payload.value(KEY_SIGNAL).toInt(-1)); + return true; + } else if (type == TYPE_SET_PROPERTY) { + // TODO: use property indices + const QString &propertyName = payload.value(KEY_PROPERTY).toString(); + const int propertyIdx = object->metaObject()->indexOfProperty(qPrintable(propertyName)); + if (propertyIdx == -1) { + qWarning() << "Cannot set unknown property" << propertyName << "of object" << objectName; + return false; + } + object->metaObject()->property(propertyIdx).write(object, payload.value(KEY_VALUE).toVariant()); + return true; + } + } + return false; +} + +QWebChannel *QMetaObjectPublisher::webChannel() const +{ + return d->webChannel; +} + +void QMetaObjectPublisher::setWebChannel(QWebChannel *webChannel) +{ + if (d->webChannel == webChannel) { + return; + } + + d->webChannel = webChannel; + + emit webChannelChanged(webChannel); +} + +bool QMetaObjectPublisher::blockUpdates() const +{ + return d->blockUpdates; +} + +void QMetaObjectPublisher::setBlockUpdates(bool block) +{ + if (d->blockUpdates == block) { + return; + } + d->blockUpdates = block; + + if (!d->blockUpdates) { + if (d->pendingInit) { + d->initializeClients(); + } else { + d->sendPendingPropertyUpdates(); + } + } else if (d->timer.isActive()) { + d->timer.stop(); + } + + emit blockUpdatesChanged(block); +} + +void QMetaObjectPublisher::bench_ensureUpdatesInitialized() +{ + if (!d->propertyUpdatesInitialized) { + d->initializeClients(); + } +} + +void QMetaObjectPublisher::bench_sendPendingPropertyUpdates() +{ + d->clientIsIdle = true; + d->sendPendingPropertyUpdates(); +} + +void QMetaObjectPublisher::bench_initializeClients() +{ + d->propertyUpdatesInitialized = false; + d->signalToPropertyMap.clear(); + d->signalHandler.clear(); + d->initializeClients(); +} + +void QMetaObjectPublisher::bench_registerObjects(const QVariantMap &objects) +{ + d->propertyUpdatesInitialized = false; + registerObjects(objects); +} + +bool QMetaObjectPublisher::test_clientIsIdle() const +{ + return d->clientIsIdle; +} + +void QMetaObjectPublisher::timerEvent(QTimerEvent *event) +{ + if (event->timerId() == d->timer.timerId()) { + d->sendPendingPropertyUpdates(); + } else { + QObject::timerEvent(event); + } +} diff --git a/src/webchannel/qmetaobjectpublisher.h b/src/webchannel/qmetaobjectpublisher.h new file mode 100644 index 0000000..da733b2 --- /dev/null +++ b/src/webchannel/qmetaobjectpublisher.h @@ -0,0 +1,114 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Copyright (C) 2013 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff +** Contact: http://www.qt-project.org/legal +* +** This file is part of the QtWebChannel 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 Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/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 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QTMETAOBJECTPUBLISHER_H +#define QTMETAOBJECTPUBLISHER_H + +#include +#include + +#include "qwebchannelglobal.h" + +class QWebChannel; + +struct QMetaObjectPublisherPrivate; + +class Q_WEBCHANNEL_EXPORT QMetaObjectPublisher : public QObject +{ + Q_OBJECT + Q_PROPERTY(QWebChannel *webChannel READ webChannel WRITE setWebChannel NOTIFY webChannelChanged); + Q_PROPERTY(bool blockUpdates READ blockUpdates WRITE setBlockUpdates NOTIFY blockUpdatesChanged); + +public: + explicit QMetaObjectPublisher(QObject *parent = 0); + virtual ~QMetaObjectPublisher(); + + Q_INVOKABLE QVariantMap classInfoForObjects(const QVariantMap &objects) const; + Q_INVOKABLE QVariantMap classInfoForObject(QObject *object) const; + + /** + * Register a map of string ID to QObject* objects. + * + * The properties, signals and public methods of the QObject are + * published to the remote client, where an object with the given identifier + * is constructed. + * + * TODO: This must be called, before clients are initialized. + */ + Q_INVOKABLE void registerObjects(const QVariantMap &objects); + + /** + * Handle the given WebChannel client request and potentially give a response. + * + * @return true if the request was handled, false otherwise. + */ + Q_INVOKABLE bool handleRequest(const QJsonObject &message); + + QWebChannel *webChannel() const; + void setWebChannel(QWebChannel *webChannel); + + /** + * When updates are blocked, no property updates are transmitted to remote clients. + */ + bool blockUpdates() const; + void setBlockUpdates(bool block); + + /// TODO: cleanup: rewrite tests in C++ and access PIMPL data from there + Q_INVOKABLE void bench_ensureUpdatesInitialized(); + Q_INVOKABLE void bench_sendPendingPropertyUpdates(); + Q_INVOKABLE void bench_registerObjects(const QVariantMap &objects); + Q_INVOKABLE void bench_initializeClients(); + Q_INVOKABLE bool test_clientIsIdle() const; + +signals: + void webChannelChanged(QWebChannel *channel); + void blockUpdatesChanged(bool block); + +protected: + void timerEvent(QTimerEvent *) Q_DECL_OVERRIDE; + +private: + QScopedPointer d; + friend struct QMetaObjectPublisherPrivate; +}; + +#endif // QMETAOBJECTPUBLISHER_H diff --git a/src/webchannel/qobject.js b/src/webchannel/qobject.js new file mode 100644 index 0000000..da8b3de --- /dev/null +++ b/src/webchannel/qobject.js @@ -0,0 +1,305 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Copyright (C) 2013 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff +** Contact: http://www.qt-project.org/legal +* +** This file is part of the QtWebChannel 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 Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/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 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +"use strict"; + +function QObject(name, data, webChannel) +{ + this.__id__ = name; + webChannel.objectMap[name] = this; + + // List of callbacks that get invoked upon signal emission + this.__objectSignals__ = {}; + + // Cache of all properties, updated when a notify signal is emitted + this.__propertyCache__ = {}; + + var object = this; + + // ---------------------------------------------------------------------- + + function unwrapQObject( response ) + { + if (!response["__QObject*__"] + || response["id"] === undefined + || response["data"] === undefined) { + return response; + } + var objectId = response.id; + if (webChannel.objectMap[objectId]) + return webChannel.objectMap[objectId]; + + var qObject = new QObject( objectId, response.data, webChannel ); + qObject.destroyed.connect(function() { + if (webChannel.objectMap[objectId] === qObject) { + delete webChannel.objectMap[objectId]; + // reset the now deleted QObject to an empty {} object + // just assigning {} though would not have the desired effect, but the + // below also ensures all external references will see the empty map + for (var prop in qObject) { + delete qObject[prop]; + } + } + }); + return qObject; + } + + function addSignal(signalData, isPropertyNotifySignal) + { + var signalName = signalData[0]; + var signalIndex = signalData[1]; + object[signalName] = { + connect: function(callback) { + if (typeof(callback) !== "function") { + console.error("Bad callback given to connect to signal " + signalName); + return; + } + + object.__objectSignals__[signalIndex] = object.__objectSignals__[signalIndex] || []; + object.__objectSignals__[signalIndex].push(callback); + + if (!isPropertyNotifySignal) { + // only required for "pure" signals, handled separately for properties in propertyUpdate + webChannel.exec({ + type: "Qt.connectToSignal", + object: object.__id__, + signal: signalIndex + }); + } + }, + disconnect: function(callback) { + if (typeof(callback) !== "function") { + console.error("Bad callback given to disconnect from signal " + signalName); + return; + } + object.__objectSignals__[signalIndex] = object.__objectSignals__[signalIndex] || []; + var idx = object.__objectSignals__[signalIndex].indexOf(callback); + if (idx === -1) { + console.error("Cannot find connection of signal " + signalName + " to " + callback.name); + return; + } + object.__objectSignals__[signalIndex].splice(idx, 1); + if (!isPropertyNotifySignal && object.__objectSignals__[signalIndex].length === 0) { + // only required for "pure" signals, handled separately for properties in propertyUpdate + webChannel.exec({ + type: "Qt.disconnectFromSignal", + object: object.__id__, + signal: signalIndex + }); + } + } + }; + } + + /** + * Invokes all callbacks for the given signalname. Also works for property notify callbacks. + */ + function invokeSignalCallbacks(signalName, signalArgs) + { + var connections = object.__objectSignals__[signalName]; + if (connections) { + connections.forEach(function(callback) { + callback.apply(callback, signalArgs); + }); + } + } + + this.propertyUpdate = function(signals, propertyMap) + { + // update property cache + for (var propertyName in propertyMap) { + var propertyValue = propertyMap[propertyName]; + object.__propertyCache__[propertyName] = propertyValue; + } + + for (var signalName in signals) { + // Invoke all callbacks, as signalEmitted() does not. This ensures the + // property cache is updated before the callbacks are invoked. + invokeSignalCallbacks(signalName, signals[signalName]); + } + } + + this.signalEmitted = function(signalName, signalArgs) + { + invokeSignalCallbacks(signalName, signalArgs); + } + + function addMethod(methodData) + { + var methodName = methodData[0]; + var methodIdx = methodData[1]; + object[methodName] = function() { + var args = []; + var callback; + for (var i = 0; i < arguments.length; ++i) { + if (typeof arguments[i] === "function") + callback = arguments[i]; + else + args.push(arguments[i]); + } + + webChannel.exec({"type": "Qt.invokeMethod", "object": object.__id__, "method": methodIdx, "args": args}, function(response) { + if ( (response !== undefined) && callback ) { + (callback)(unwrapQObject(response)); + } + }); + }; + } + + function bindGetterSetter(propertyInfo) + { + var propertyName = propertyInfo[0]; + var notifySignalData = propertyInfo[1]; + // initialize property cache with current value + object.__propertyCache__[propertyName] = propertyInfo[2] + + if (notifySignalData) { + if (notifySignalData[0] === 1) { + // signal name is optimized away, reconstruct the actual name + notifySignalData[0] = propertyName + "Changed"; + } + addSignal(notifySignalData, true); + } + + object.__defineSetter__(propertyName, function(value) { + if (value === undefined) { + console.warn("Property setter for " + propertyName + " called with undefined value!"); + return; + } + object.__propertyCache__[propertyName] = value; + webChannel.exec({"type": "Qt.setProperty", "object": object.__id__, "property": propertyName, "value": value }); + + }); + object.__defineGetter__(propertyName, function () { + return (function (callback) { + var propertyValue = object.__propertyCache__[propertyName]; + if (propertyValue === undefined) { + // This shouldn't happen + console.warn("Undefined value in property cache for property \"" + propertyName + "\" in object " + object.__id__); + } + + // TODO: A callback is not required here anymore, but is kept for backwards compatibility + if (callback !== undefined) { + if (typeof(callback) !== "function") { + console.error("Bad callback given to get property " + property); + return; + } + callback(propertyValue); + } else { + return propertyValue; + } + }); + }); + } + + // ---------------------------------------------------------------------- + + data.methods.forEach(addMethod); + + data.properties.forEach(bindGetterSetter); + + data.signals.forEach(function(signal) { addSignal(signal, false); }); + + for (var name in data.enums) { + object[name] = data.enums[name]; + } +} + +window.setupQObjectWebChannel = function(webChannel, doneCallback) +{ + // prevent multiple initialization which might happen with multiple webchannel clients. + var initialized = false; + + webChannel.subscribe( + "Qt.signal", + function(payload) { + var object = window[payload.object] || webChannel.objectMap[payload.object]; + if (object) { + object.signalEmitted(payload.signal, payload.args); + } else { + console.warn("Unhandled signal: " + payload.object + "::" + payload.signal); + } + } + ); + + webChannel.subscribe( + "Qt.propertyUpdate", + function(payload) { + for (var i in payload) { + var data = payload[i]; + var object = window[data.object] || webChannel.objectMap[data.object]; + if (object) { + object.propertyUpdate(data.signals, data.properties); + } else { + console.warn("Unhandled property update: " + data.object + "::" + data.signal); + } + } + setTimeout(function() { webChannel.exec({type: "Qt.idle"}); }, 0); + } + ); + + webChannel.subscribe( + "Qt.init", + function(payload) { + if (initialized) { + return; + } + initialized = true; + for (var objectName in payload) { + var data = payload[objectName]; + var object = new QObject(objectName, data, webChannel); + window[objectName] = object; + } + if (doneCallback) { + doneCallback(); + } + setTimeout(function() { webChannel.exec({type: "Qt.idle"}); }, 0); + } + ); + + webChannel.exec({type:"Qt.init"}); + + webChannel.debug = function(message) + { + webChannel.send({"data" : {"type" : "Qt.Debug", "message" : message}}); + }; +}; diff --git a/src/webchannel/qwebchannel.cpp b/src/webchannel/qwebchannel.cpp new file mode 100644 index 0000000..8dc7e5e --- /dev/null +++ b/src/webchannel/qwebchannel.cpp @@ -0,0 +1,207 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Copyright (C) 2013 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff +** Contact: http://www.qt-project.org/legal +* +** This file is part of the QtWebChannel 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 Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/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 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qwebchannel.h" + +#include +#include +#include +#include +#include + +#include "qwebsocketserver_p.h" + +class QWebChannelPrivate : public QWebSocketServer +{ + Q_OBJECT +public: + QByteArray m_secret; + bool m_useSecret; + + QString m_baseUrl; + bool m_starting; + + QWebChannelPrivate(QObject* parent) + : QWebSocketServer(parent) + , m_useSecret(true) + , m_starting(false) + { + connect(this, SIGNAL(error(QAbstractSocket::SocketError)), + SLOT(socketError())); + } + + void initLater() + { + if (m_starting) + return; + metaObject()->invokeMethod(this, "init", Qt::QueuedConnection); + m_starting = true; + } + + void sendJSONMessage(const QJsonValue& id, const QJsonValue& data, bool response) const; + +signals: + void failed(const QString& reason); + void initialized(); + +protected: + bool isValid(const HeaderData& connection) Q_DECL_OVERRIDE; + +private slots: + void init(); + void socketError(); +}; + +bool QWebChannelPrivate::isValid(const HeaderData& connection) +{ + if (!QWebSocketServer::isValid(connection)) { + return false; + } + return connection.protocol == QByteArrayLiteral("QWebChannel") + && connection.path == m_secret; +} + +void QWebChannelPrivate::init() +{ + close(); + + m_starting = false; + if (m_useSecret) { + m_secret = QUuid::createUuid().toByteArray(); + // replace { by / + m_secret[0] = '/'; + // chop of trailing } + m_secret.chop(1); + } + + if (!listen(QHostAddress::LocalHost)) { + emit failed(errorString()); + return; + } + + m_baseUrl = QStringLiteral("127.0.0.1:%1%2").arg(port()).arg(QString::fromLatin1(m_secret)); + emit initialized(); +} + +void QWebChannelPrivate::socketError() +{ + emit failed(errorString()); +} + +void QWebChannelPrivate::sendJSONMessage(const QJsonValue& id, const QJsonValue& data, bool response) const +{ + QJsonObject obj; + if (response) { + obj[QStringLiteral("response")] = true; + } + obj[QStringLiteral("id")] = id; + if (!data.isNull()) { + obj[QStringLiteral("data")] = data; + } + QJsonDocument doc(obj); + sendMessage(doc.toJson(QJsonDocument::Compact)); +} + +QWebChannel::QWebChannel(QObject *parent) +: QObject(parent) +, d(new QWebChannelPrivate(this)) +{ + connect(d, SIGNAL(textDataReceived(QString)), + SIGNAL(rawMessageReceived(QString))); + connect(d, SIGNAL(failed(QString)), + SIGNAL(failed(QString))); + connect(d, SIGNAL(initialized()), + SLOT(onInitialized())); + connect(d, SIGNAL(pongReceived()), + SIGNAL(pongReceived())); + d->initLater(); +} + +QWebChannel::~QWebChannel() +{ +} + +QString QWebChannel::baseUrl() const +{ + return d->m_baseUrl; +} + +void QWebChannel::setUseSecret(bool s) +{ + if (d->m_useSecret == s) + return; + d->m_useSecret = s; + d->initLater(); +} + +bool QWebChannel::useSecret() const +{ + return d->m_useSecret; +} + +void QWebChannel::onInitialized() +{ + emit initialized(); + emit baseUrlChanged(d->m_baseUrl); +} + +void QWebChannel::respond(const QJsonValue& messageId, const QJsonValue& data) const +{ + d->sendJSONMessage(messageId, data, true); +} + +void QWebChannel::sendMessage(const QJsonValue& id, const QJsonValue& data) const +{ + d->sendJSONMessage(id, data, false); +} + +void QWebChannel::sendRawMessage(const QString& message) const +{ + d->sendMessage(message.toUtf8()); +} + +void QWebChannel::ping() const +{ + d->ping(); +} + +#include "qwebchannel.moc" diff --git a/src/webchannel/qwebchannel.h b/src/webchannel/qwebchannel.h new file mode 100644 index 0000000..321706e --- /dev/null +++ b/src/webchannel/qwebchannel.h @@ -0,0 +1,91 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Copyright (C) 2013 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff +** Contact: http://www.qt-project.org/legal +* +** This file is part of the QtWebChannel 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 Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/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 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QWEBCHANNEL_H +#define QWEBCHANNEL_H + +#include +#include + +#include "qwebchannelglobal.h" + +class QWebChannelPrivate; + +class Q_WEBCHANNEL_EXPORT QWebChannel : public QObject +{ + Q_OBJECT + Q_DISABLE_COPY(QWebChannel) + Q_PROPERTY(QString baseUrl READ baseUrl NOTIFY baseUrlChanged) + Q_PROPERTY(bool useSecret READ useSecret WRITE setUseSecret) + +public: + QWebChannel(QObject *parent = 0); + ~QWebChannel(); + + QString baseUrl() const; + + void setUseSecret(bool); + bool useSecret() const; + +signals: + void baseUrlChanged(const QString& baseUrl); + void rawMessageReceived(const QString& rawMessage); + void pongReceived(); + void initialized(); + + void failed(const QString& reason); + +public slots: + void sendMessage(const QJsonValue& id, const QJsonValue& data = QJsonValue()) const; + void respond(const QJsonValue& messageId, const QJsonValue& data = QJsonValue()) const; + void sendRawMessage(const QString& rawMessage) const; + void ping() const; + +private slots: + void onInitialized(); + +private: + QWebChannelPrivate* d; +}; + +#endif // QWEBCHANNEL_H + diff --git a/src/webchannel/qwebchannelglobal.h b/src/webchannel/qwebchannelglobal.h new file mode 100644 index 0000000..f33ada6 --- /dev/null +++ b/src/webchannel/qwebchannelglobal.h @@ -0,0 +1,62 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Copyright (C) 2013 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff +** Contact: http://www.qt-project.org/legal +* +** This file is part of the QtWebChannel 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 Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/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 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QTWEBCHANNEL_H +#define QTWEBCHANNEL_H + +#include + +QT_BEGIN_NAMESPACE + +#ifndef QT_STATIC +# if defined(QT_BUILD_WEBCHANNEL_LIB) +# define Q_WEBCHANNEL_EXPORT Q_DECL_EXPORT +# else +# define Q_WEBCHANNEL_EXPORT Q_DECL_IMPORT +# endif +#else +# define Q_WEBCHANNEL_EXPORT +#endif + +QT_END_NAMESPACE + +#endif // QTWEBCHANNEL_H diff --git a/src/webchannel/qwebsocketserver.cpp b/src/webchannel/qwebsocketserver.cpp new file mode 100644 index 0000000..4bc56c8 --- /dev/null +++ b/src/webchannel/qwebsocketserver.cpp @@ -0,0 +1,424 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Copyright (C) 2013 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff +** Contact: http://www.qt-project.org/legal +* +** This file is part of the QtWebChannel 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 Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/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 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qwebsocketserver_p.h" + +#include +#include +#include +#include + +#include + +namespace { +template +inline static void appendBytes(QByteArray& data, T value) +{ + data.append(reinterpret_cast(&value), sizeof(value)); +} + +inline static void unmask(QByteArray& data, char mask[4]) +{ + for (int i = 0; i < data.size(); ++i) { + int j = i % 4; + data[i] = data[i] ^ mask[j]; + } +} + +inline static char bitMask(int bit) +{ + return 1 << bit; +} + +// see: http://tools.ietf.org/html/rfc6455#page-28 +static const char FIN_BIT = bitMask(7); +static const char MASKED_BIT = bitMask(7); +static const char OPCODE_RANGE = bitMask(4) - 1; +static const char PAYLOAD_RANGE = bitMask(7) - 1; +static const char EXTENDED_PAYLOAD = 126; +static const char EXTENDED_LONG_PAYLOAD = 127; +} + +QWebSocketServer::QWebSocketServer(QObject* parent) +: QObject(parent) +, m_server(new QTcpServer(this)) +{ + connect(m_server, SIGNAL(newConnection()), + SLOT(newConnection())); + connect(m_server, SIGNAL(acceptError(QAbstractSocket::SocketError)), + SIGNAL(error(QAbstractSocket::SocketError))); +} + +QWebSocketServer::~QWebSocketServer() +{ + close(); +} + +bool QWebSocketServer::listen(const QHostAddress& address, quint16 port) +{ + return m_server->listen(address, port); +} + +void QWebSocketServer::close() +{ + sendFrame(Frame::ConnectionClose, QByteArray()); + m_server->close(); +} + +quint16 QWebSocketServer::port() const +{ + return m_server->serverPort(); +} + +QHostAddress QWebSocketServer::address() const +{ + return m_server->serverAddress(); +} + +QString QWebSocketServer::errorString() const +{ + return m_server->errorString(); +} + +void QWebSocketServer::newConnection() +{ + if (!m_server->hasPendingConnections()) + return; + + QTcpSocket* connection = m_server->nextPendingConnection(); + m_connections.insert(connection, Connection()); + connect(connection, SIGNAL(readyRead()), + SLOT(readSocketData())); + connect(connection, SIGNAL(error(QAbstractSocket::SocketError)), + SIGNAL(error(QAbstractSocket::SocketError))); + connect(connection, SIGNAL(disconnected()), + SLOT(disconnected())); +} + +void QWebSocketServer::disconnected() +{ + QTcpSocket* socket = qobject_cast(sender()); + Q_ASSERT(socket); + + m_connections.remove(socket); +} + +static const QByteArray headerSwitchProtocols = QByteArrayLiteral("HTTP/1.1 101 Switching Protocols"); +static const QByteArray headerGet = QByteArrayLiteral("GET "); +static const QByteArray headerHTTP = QByteArrayLiteral("HTTP/1.1"); +static const QByteArray headerHost = QByteArrayLiteral("Host: "); +static const QByteArray headerUpgrade = QByteArrayLiteral("Upgrade: websocket"); +static const QByteArray headerConnection = QByteArrayLiteral("Connection: Upgrade"); +static const QByteArray headerSecKey = QByteArrayLiteral("Sec-WebSocket-Key: "); +static const QByteArray headerSecProtocol = QByteArrayLiteral("Sec-WebSocket-Protocol: "); +static const QByteArray headerSecVersion = QByteArrayLiteral("Sec-WebSocket-Version: 13"); +static const QByteArray headerSecAccept = QByteArrayLiteral("Sec-WebSocket-Accept: "); +static const QByteArray headerOrigin = QByteArrayLiteral("Origin: "); +static const QByteArray headerMagicKey = QByteArrayLiteral("258EAFA5-E914-47DA-95CA-C5AB0DC85B11"); +static const QByteArray headerEOL = QByteArrayLiteral("\r\n"); +static const QByteArray httpBadRequest = QByteArrayLiteral("HTTP/1.1 400 Bad Request\r\n"); + +void QWebSocketServer::readSocketData() +{ + QTcpSocket* socket = qobject_cast(sender()); + Q_ASSERT(socket); + + Connection& connection = m_connections[socket]; + + if (!connection.header.wasUpgraded) { + readHeaderData(socket, connection.header); + } + + if (connection.header.wasUpgraded) { + while (socket->bytesAvailable()) { + if (!readFrameData(socket, connection.currentFrame)) { + close(socket, connection.header); + } + } + } +} + +void QWebSocketServer::readHeaderData(QTcpSocket* socket, HeaderData& header) +{ + while (socket->canReadLine()) { + QByteArray line = socket->readLine().trimmed(); + if (line.isEmpty()) { + // finalize + if (isValid(header)) { + upgrade(socket, header); + } else { + close(socket, header); + } + break; + } else if (line.startsWith(headerGet) && line.endsWith(headerHTTP)) { + header.path = line.mid(headerGet.size(), line.size() - headerGet.size() - headerHTTP.size()).trimmed(); + } else if (line.startsWith(headerHost)) { + header.host = line.mid(headerHost.size()).trimmed(); + } else if (line.startsWith(headerSecKey)) { + header.key = line.mid(headerSecKey.size()).trimmed(); + } else if (line.startsWith(headerOrigin)) { + header.origin = line.mid(headerOrigin.size()).trimmed(); + } else if (line.startsWith(headerSecProtocol)) { + header.protocol = line.mid(headerSecProtocol.size()).trimmed(); + } else if (line == headerUpgrade) { + header.hasUpgrade = true; + } else if (line == headerConnection) { + header.hasConnection = true; + } else if (line == headerSecVersion) { + header.hasVersion = true; + } else { + header.otherHeaders << line; + } + } +} + +// see: http://tools.ietf.org/html/rfc6455#page-28 +bool QWebSocketServer::readFrameData(QTcpSocket* socket, Frame& frame) +{ + int bytesAvailable = socket->bytesAvailable(); + if (frame.state == Frame::ReadStart) { + if (bytesAvailable < 2) { + return true; + } + uchar buffer[2]; + socket->read(reinterpret_cast(buffer), 2); + bytesAvailable -= 2; + frame.fin = buffer[0] & FIN_BIT; + // skip rsv1, rsv2, rsv3 + // last four bits are the opcode + quint8 opcode = buffer[0] & OPCODE_RANGE; + if (opcode != Frame::ContinuationFrame && opcode != Frame::BinaryFrame && + opcode != Frame::ConnectionClose && opcode != Frame::TextFrame && + opcode != Frame::Ping && opcode != Frame::Pong) + { + qWarning() << "invalid opcode: " << opcode; + return false; + } + frame.opcode = static_cast(opcode); + // test first, i.e. highest bit for mask + frame.masked = buffer[1] & MASKED_BIT; + if (!frame.masked) { + qWarning() << "unmasked frame received"; + return false; + } + // final seven bits are the payload length + frame.length = static_cast(buffer[1] & PAYLOAD_RANGE); + if (frame.length == EXTENDED_PAYLOAD) { + frame.state = Frame::ReadExtendedPayload; + } else if (frame.length == EXTENDED_LONG_PAYLOAD) { + frame.state = Frame::ReadExtendedLongPayload; + } else { + frame.state = Frame::ReadMask; + } + } + if (frame.state == Frame::ReadExtendedPayload) { + if (bytesAvailable < 2) { + return true; + } + uchar buffer[2]; + socket->read(reinterpret_cast(buffer), 2); + bytesAvailable -= 2; + frame.length = qFromBigEndian(buffer); + frame.state = Frame::ReadMask; + } + if (frame.state == Frame::ReadExtendedLongPayload) { + if (bytesAvailable < 8) { + return true; + } + uchar buffer[8]; + socket->read(reinterpret_cast(buffer), 8); + bytesAvailable -= 8; + quint64 longSize = qFromBigEndian(buffer); + // QByteArray uses int for size type so limit ourselves to that size as well + if (longSize > static_cast(std::numeric_limits::max())) { + return false; + } + frame.length = static_cast(longSize); + frame.state = Frame::ReadMask; + } + if (frame.state == Frame::ReadMask) { + if (bytesAvailable < 4) { + return true; + } + socket->read(frame.mask, 4); + bytesAvailable -= 4; + frame.state = Frame::ReadData; + frame.data.reserve(frame.length); + } + if (frame.state == Frame::ReadData && (bytesAvailable || !frame.length)) { + frame.data.append(socket->read(qMin(frame.length - frame.data.size(), bytesAvailable))); + if (frame.data.size() == frame.length) { + frame.state = Frame::ReadStart; + handleFrame(socket, frame); + } + } + return true; +} + +void QWebSocketServer::handleFrame(QTcpSocket* socket, Frame& frame) +{ + unmask(frame.data, frame.mask); + + // fragmentation support - see http://tools.ietf.org/html/rfc6455#page-33 + if (!frame.fin) { + if (frame.opcode != Frame::ContinuationFrame) { + frame.initialOpcode = frame.opcode; + } + frame.fragments += frame.data; + } else if (frame.fin && frame.opcode == Frame::ContinuationFrame) { + frame.opcode = frame.initialOpcode; + frame.data = frame.fragments + frame.data; + } // otherwise if it's fin and a non-continuation frame its a single-frame message + + switch (frame.opcode) { + case Frame::ContinuationFrame: + // do nothing + break; + case Frame::Ping: + socket->write(frameHeader(Frame::Pong, 0)); + break; + case Frame::Pong: + emit pongReceived(); + break; + case Frame::ConnectionClose: + ///TODO: handle? + qWarning("Unhandled connection close frame"); + break; + case Frame::BinaryFrame: + emit binaryDataReceived(frame.data); + break; + case Frame::TextFrame: + emit textDataReceived(QString::fromUtf8(frame.data)); + break; + } + + if (frame.fin) { + frame = Frame(); + } +} + +bool QWebSocketServer::isValid(const HeaderData& header) +{ + return !header.path.isEmpty() && !header.host.isEmpty() && !header.key.isEmpty() + && header.hasUpgrade && header.hasConnection && header.hasVersion; +} + +void QWebSocketServer::close(QTcpSocket* socket, const HeaderData& header) +{ + if (header.wasUpgraded) { + //TODO: implement this properly - see http://tools.ietf.org/html/rfc6455#page-36 + socket->write(frameHeader(Frame::Frame::ConnectionClose, 0)); + } else { + socket->write(httpBadRequest); + } + socket->close(); +} + +void QWebSocketServer::upgrade(QTcpSocket* socket, HeaderData& header) +{ + socket->write(headerSwitchProtocols); + socket->write(headerEOL); + + socket->write(headerUpgrade); + socket->write(headerEOL); + + socket->write(headerConnection); + socket->write(headerEOL); + + socket->write(headerSecAccept); + socket->write(QCryptographicHash::hash( header.key + headerMagicKey, QCryptographicHash::Sha1 ).toBase64()); + socket->write(headerEOL); + + if (!header.protocol.isEmpty()) { + socket->write(headerSecProtocol); + socket->write(header.protocol); + socket->write(headerEOL); + } + + socket->write(headerEOL); + + header.wasUpgraded = true; +} + +void QWebSocketServer::sendMessage(const QByteArray& message) const +{ + sendFrame(Frame::TextFrame, message); +} + +void QWebSocketServer::sendFrame(Frame::Opcode opcode, const QByteArray& data) const +{ + const QByteArray& header = frameHeader(opcode, data.size()); + QHash< QTcpSocket*, Connection >::const_iterator it = m_connections.constBegin(); + while (it != m_connections.constEnd()) { + if (it.value().header.wasUpgraded) { + it.key()->write(header); + it.key()->write(data); + } + ++it; + } +} + +// see: http://tools.ietf.org/html/rfc6455#page-28 +QByteArray QWebSocketServer::frameHeader(QWebSocketServer::Frame::Opcode opcode, const int dataSize) const +{ + // we only support single frames for now + Q_ASSERT(opcode != Frame::ContinuationFrame); + + QByteArray header; + header.reserve(4); + header.append(FIN_BIT | opcode); + if (dataSize < EXTENDED_PAYLOAD) { + header.append(static_cast(dataSize)); + } else if (dataSize < std::numeric_limits::max()) { + header.append(EXTENDED_PAYLOAD); + appendBytes(header, qToBigEndian(dataSize)); + } else { + header.append(EXTENDED_LONG_PAYLOAD); + appendBytes(header, qToBigEndian(dataSize)); + } + return header; +} + +void QWebSocketServer::ping() const +{ + sendFrame(Frame::Ping, QByteArray()); +} diff --git a/src/webchannel/qwebsocketserver_p.h b/src/webchannel/qwebsocketserver_p.h new file mode 100644 index 0000000..4651d17 --- /dev/null +++ b/src/webchannel/qwebsocketserver_p.h @@ -0,0 +1,160 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Copyright (C) 2013 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff +** Contact: http://www.qt-project.org/legal +* +** This file is part of the QtWebChannel 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 Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/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 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QWEBSOCKET_H +#define QWEBSOCKET_H + +#include +#include + +class QTcpServer; +class QTcpSocket; + +class QWebSocketServer : public QObject +{ + Q_OBJECT + +public: + explicit QWebSocketServer(QObject* parent = 0); + virtual ~QWebSocketServer(); + + bool listen(const QHostAddress& address = QHostAddress::LocalHost, quint16 port = 0); + void close(); + + QHostAddress address() const; + quint16 port() const; + + QString errorString() const; + +signals: + void opened(); + void error(QAbstractSocket::SocketError); + void textDataReceived(const QString& data); + void binaryDataReceived(const QByteArray& data); + void pongReceived(); + +public slots: + void sendMessage(const QByteArray& message) const; + void ping() const; + +private slots: + void newConnection(); + void readSocketData(); + void disconnected(); + +protected: + struct HeaderData + { + HeaderData() + : hasVersion(false) + , hasUpgrade(false) + , hasConnection(false) + , wasUpgraded(false) + { + } + QByteArray path; + QByteArray host; + QByteArray origin; + QByteArray key; + QByteArray protocol; + QVector otherHeaders; + // no bitmap here - we only have few of these objects + bool hasVersion; + bool hasUpgrade; + bool hasConnection; + bool wasUpgraded; + }; + virtual bool isValid(const HeaderData& connection); + +private: + struct Frame + { + enum State { + ReadStart, + ReadExtendedPayload, + ReadExtendedLongPayload, + ReadMask, + ReadData, + Finished + }; + enum Opcode { + ContinuationFrame = 0x0, + TextFrame = 0x1, + BinaryFrame = 0x2, + ConnectionClose = 0x8, + Ping = 0x9, + Pong = 0xA + }; + // no bitmap here - we only have a few of these objects + State state; + Opcode opcode; + bool fin; + bool masked; + ///NOTE: standard says unsigned 64bit integer but QByteArray only supports 'int' size + int length; + char mask[4]; + QByteArray data; + // fragmentation support + Opcode initialOpcode; + QByteArray fragments; + }; + struct Connection + { + HeaderData header; + Frame currentFrame; + }; + + void readHeaderData(QTcpSocket* socket, HeaderData& header); + void close(QTcpSocket* socket, const HeaderData& header); + void upgrade(QTcpSocket* socket, HeaderData& header); + bool readFrameData(QTcpSocket* socket, Frame& frame); + void handleFrame(QTcpSocket* socket, Frame& frame); + + void sendFrame(Frame::Opcode opcode, const QByteArray& data) const; + void sendFrame(QTcpSocket* socket, Frame::Opcode opcode, const QByteArray& data) const; + QByteArray frameHeader(Frame::Opcode opcode, const int dataSize) const; + + QTcpServer* m_server; + QHash m_connections; +}; + +#endif // QWEBSOCKET_H diff --git a/src/webchannel/resources.qrc b/src/webchannel/resources.qrc new file mode 100644 index 0000000..821e911 --- /dev/null +++ b/src/webchannel/resources.qrc @@ -0,0 +1,6 @@ + + + webchannel.js + qobject.js + + diff --git a/src/webchannel/signalhandler_p.h b/src/webchannel/signalhandler_p.h new file mode 100644 index 0000000..2613f92 --- /dev/null +++ b/src/webchannel/signalhandler_p.h @@ -0,0 +1,278 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Copyright (C) 2013 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff +** Contact: http://www.qt-project.org/legal +* +** This file is part of the QtWebChannel 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 Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/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 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef SIGNALHANDLER_H +#define SIGNALHANDLER_H + +#include +#include +#include +#include +#include + +/** + * The signal handler is similar to QSignalSpy, but geared towards the usecase of the web channel. + * + * It allows connecting to any number of signals of arbitrary objects and forwards the signal + * invocations to the Receiver by calling its signalEmitted function, which takes the object, + * signal index and a QVariantList of arguments. + */ +template +class SignalHandler : public QObject +{ +public: + SignalHandler(Receiver *receiver, QObject *parent = 0) + : QObject(parent) + , m_receiver(receiver) + { + } + + /** + * Connect to a signal of @p object identified by @p signalIndex. + * + * If the handler is already connected to the signal, an internal counter is increased, + * i.e. the handler never connects multiple times to the same signal. + */ + void connectTo(const QObject *object, const int signalIndex); + + /** + * Decrease the connection counter for the connection to the given signal. + * + * When the counter drops to zero, the connection is disconnected. + */ + void disconnectFrom(const QObject *object, const int signalIndex); + + /** + * @internal + * + * Custom implementation of qt_metacall which calls dispatch() for connected signals. + */ + int qt_metacall(QMetaObject::Call call, int methodId, void **args) Q_DECL_OVERRIDE; + + /** + * Reset all connections, useful for benchmarks. + */ + void clear(); + +private: + /** + * Exctract the arguments of a signal call and pass them to the receiver. + * + * The @p argumentData is converted to a QVariantList and then passed to the receiver's + * signalEmitted method. + */ + void dispatch(const QObject *object, const int signalIdx, void **argumentData); + + Receiver *m_receiver; + + // maps meta object -> signalIndex -> list of arguments + // NOTE: This data is "leaked" on disconnect until deletion of the handler, is this a problem? + typedef QVector ArgumentTypeList; + typedef QHash SignalArgumentHash; + QHash m_signalArgumentTypes; + + /* + * Tracks how many connections are active to object signals. + * + * Maps object -> signalIndex -> pair of connection and number of connections + * + * Note that the handler is connected to the signal only once, whereas clients + * may have connected multiple times. + * + * TODO: Move more of this logic to the HTML client side, esp. the connection counting. + */ + typedef QPair ConnectionPair; + typedef QHash SignalConnectionHash; + typedef QHash ConnectionHash; + ConnectionHash m_connectionsCounter; +}; + +/** + * Find and return the signal of index @p signalIndex in the meta object of @p object and return it. + * + * The return value is also verified to ensure it is a signal. + */ +QMetaMethod findSignal(const QMetaObject *metaObject, const int signalIndex) +{ + QMetaMethod signal = metaObject->method(signalIndex); + if (!signal.isValid()) { + qWarning("Cannot find signal with index %d of object %s", signalIndex, metaObject->className()); + return QMetaMethod(); + } + Q_ASSERT(signal.methodType() == QMetaMethod::Signal); + return signal; +} + +template +void SignalHandler::connectTo(const QObject *object, const int signalIndex) +{ + const QMetaObject *metaObject = object->metaObject(); + const QMetaMethod &signal = findSignal(metaObject, signalIndex); + if (!signal.isValid()) { + return; + } + + ConnectionPair &connectionCounter = m_connectionsCounter[object][signalIndex]; + if (connectionCounter.first) { + // increase connection counter if already connected + ++connectionCounter.second; + return; + } // otherwise not yet connected, do so now + + const int memberOffset = QObject::staticMetaObject.methodCount(); + QMetaObject::Connection connection = QMetaObject::connect(object, signal.methodIndex(), this, memberOffset, Qt::DirectConnection, 0); + if (!connection) { + qWarning() << "SignalHandler: QMetaObject::connect returned false. Unable to connect to" << object << signal.name() << signal.methodSignature(); + return; + } + connectionCounter.first = connection; + connectionCounter.second = 1; + + if (!m_signalArgumentTypes.value(metaObject).contains(signal.methodIndex())) { + // find the type ids of the signal parameters, see also QSignalSpy::initArgs + QVector args; + args.reserve(signal.parameterCount()); + for (int i = 0; i < signal.parameterCount(); ++i) { + int tp = signal.parameterType(i); + if (tp == QMetaType::UnknownType && object) { + void *argv[] = { &tp, &i }; + QMetaObject::metacall(const_cast(object), + QMetaObject::RegisterMethodArgumentMetaType, + signal.methodIndex(), argv); + if (tp == -1) { + tp = QMetaType::UnknownType; + } + } + if (tp == QMetaType::UnknownType) { + Q_ASSERT(tp != QMetaType::Void); // void parameter => metaobject is corrupt + qWarning("Don't know how to handle '%s', use qRegisterMetaType to register it.", + signal.parameterNames().at(i).constData()); + } + args << tp; + } + + m_signalArgumentTypes[metaObject][signal.methodIndex()] = args; + } +} + +template +void SignalHandler::dispatch(const QObject *object, const int signalIdx, void **argumentData) +{ + Q_ASSERT(m_signalArgumentTypes.contains(object->metaObject())); + const QHash > &objectSignalArgumentTypes = m_signalArgumentTypes.value(object->metaObject()); + QHash >::const_iterator signalIt = objectSignalArgumentTypes.constFind(signalIdx); + if (signalIt == objectSignalArgumentTypes.constEnd()) { + // not connected to this signal, skip + return; + } + const QVector &argumentTypes = *signalIt; + QVariantList arguments; + arguments.reserve(argumentTypes.count()); + // TODO: basic overload resolution based on number of arguments? + for (int i = 0; i < argumentTypes.count(); ++i) { + const QMetaType::Type type = static_cast(argumentTypes.at(i)); + QVariant arg; + if (type == QMetaType::QVariant) { + arg = *reinterpret_cast(argumentData[i + 1]); + } else { + arg = QVariant(type, argumentData[i + 1]); + } + arguments.append(arg); + } + m_receiver->signalEmitted(object, signalIdx, arguments); +} + +template +void SignalHandler::disconnectFrom(const QObject *object, const int signalIndex) +{ + Q_ASSERT(m_connectionsCounter.value(object).contains(signalIndex)); + ConnectionPair &connection = m_connectionsCounter[object][signalIndex]; + --connection.second; + if (!connection.second || !connection.first) { + QObject::disconnect(connection.first); + m_connectionsCounter[object].remove(signalIndex); + if (m_connectionsCounter[object].isEmpty()) { + m_connectionsCounter.remove(object); + } + } +} + +template +int SignalHandler::qt_metacall(QMetaObject::Call call, int methodId, void **args) +{ + methodId = QObject::qt_metacall(call, methodId, args); + if (methodId < 0) + return methodId; + + if (call == QMetaObject::InvokeMetaMethod) { + if (methodId == 0) { + Q_ASSERT(sender()); + Q_ASSERT(senderSignalIndex() != -1); + dispatch(sender(), senderSignalIndex(), args); + static const int destroyedIndex = metaObject()->indexOfSignal("destroyed(QObject*)"); + if (senderSignalIndex() == destroyedIndex) { + ConnectionHash::iterator it = m_connectionsCounter.find(sender()); + Q_ASSERT(it != m_connectionsCounter.end()); + foreach (const ConnectionPair &connection, *it) { + QObject::disconnect(connection.first); + } + m_connectionsCounter.erase(it); + } + } + --methodId; + } + return methodId; +} + +template +void SignalHandler::clear() +{ + foreach (const SignalConnectionHash &connections, m_connectionsCounter) { + foreach (const ConnectionPair &connection, connections) { + QObject::disconnect(connection.first); + } + } + m_connectionsCounter.clear(); + m_signalArgumentTypes.clear(); +} + +#endif // SIGNALHANDLER_H diff --git a/src/webchannel/variantargument_p.h b/src/webchannel/variantargument_p.h new file mode 100644 index 0000000..ee625fc --- /dev/null +++ b/src/webchannel/variantargument_p.h @@ -0,0 +1,103 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Copyright (C) 2013 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff +** Contact: http://www.qt-project.org/legal +* +** This file is part of the QtWebChannel 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 Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/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 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef VARIANTARGUMENT_H +#define VARIANTARGUMENT_H + +#include +#include + +/** + * RAII QVariant to Q[Generic]Argument conversion + */ +class VariantArgument +{ +public: + explicit VariantArgument() + : m_data(0) + , m_paramType(0) + { + } + + /// TODO: test with C++ methods that don't take a QVariant as arg + /// also test conversions + void setValue(const QVariant &value, int paramType) + { + if (m_data) { + QMetaType::destroy(m_paramType, m_data); + m_name.clear(); + m_data = 0; + } + + m_paramType = paramType; + + if (value.isValid()) { + m_name = value.typeName(); + m_data = QMetaType::create(m_paramType, value.constData()); + } + } + + ~VariantArgument() + { + if (m_data) { + QMetaType::destroy(m_paramType, m_data); + m_data = 0; + } + } + + operator QGenericArgument() const + { + if (!m_data) { + return QGenericArgument(); + } + return QGenericArgument(m_name.constData(), m_data); + } + +private: + Q_DISABLE_COPY(VariantArgument) + + QByteArray m_name; + void* m_data; + int m_paramType; +}; + +#endif // VARIANTARGUMENT_H diff --git a/src/webchannel/webchannel.js b/src/webchannel/webchannel.js new file mode 100644 index 0000000..8827cbf --- /dev/null +++ b/src/webchannel/webchannel.js @@ -0,0 +1,129 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Copyright (C) 2013 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff +** Contact: http://www.qt-project.org/legal +* +** This file is part of the QtWebChannel 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 Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/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 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +"use strict"; + +var QWebChannel = function(baseUrl, initCallback) +{ + var channel = this; + // support multiple channels listening to the same socket + // the responses to channel.exec must be distinguishable + // see: http://stackoverflow.com/a/2117523/35250 + this.id = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8); + return v.toString(16); + }); + ///TODO: use ssl? + var socketUrl = "ws://" + baseUrl; + this.socket = new WebSocket(socketUrl, "QWebChannel"); + this.send = function(data) + { + if (typeof(data) !== "string") { + data = JSON.stringify(data); + } + channel.socket.send(data); + }; + + this.socket.onopen = function() + { + initCallback(channel); + }; + this.socket.onclose = function() + { + console.error("web channel closed"); + }; + this.socket.onerror = function(error) + { + console.error("web channel error: " + error); + }; + this.socket.onmessage = function(message) + { + var jsonData = JSON.parse(message.data); + if (jsonData.id === undefined) { + console.error("invalid message received:", message.data); + return; + } + if (jsonData.data === undefined) { + jsonData.data = {}; + } + if (jsonData.response) { + if (jsonData.id[0] === channel.id) { + channel.execCallbacks[jsonData.id[1]](jsonData.data); + delete channel.execCallbacks[jsonData.id]; + } + } else if (channel.subscriptions[jsonData.id]) { + channel.subscriptions[jsonData.id].forEach(function(callback) { + (callback)(jsonData.data); } + ); + } + }; + + this.subscriptions = {}; + this.subscribe = function(id, callback) + { + if (channel.subscriptions[id]) { + channel.subscriptions[id].push(callback); + } else { + channel.subscriptions[id] = [callback]; + } + }; + + this.execCallbacks = {}; + this.execId = 0; + this.exec = function(data, callback) + { + if (!callback) { + // if no callback is given, send directly + channel.send({data: data}); + return; + } + if (channel.execId === Number.MAX_VALUE) { + // wrap + channel.execId = Number.MIN_VALUE; + } + var id = channel.execId++; + channel.execCallbacks[id] = callback; + channel.send({"id": [channel.id, id], "data": data}); + }; + + this.objectMap = {}; +}; diff --git a/src/webchannel/webchannel.pro b/src/webchannel/webchannel.pro new file mode 100644 index 0000000..a0041bf --- /dev/null +++ b/src/webchannel/webchannel.pro @@ -0,0 +1,28 @@ +TARGET = QtWebChannel +QT = core network +CONFIG += warn_on strict_flags + +load(qt_module) + +RESOURCES += \ + resources.qrc + +OTHER_FILES = \ + webchannel.js \ + qobject.js + +PUBLIC_HEADERS += \ + qwebchannel.h \ + qmetaobjectpublisher.h + +PRIVATE_HEADERS += \ + qwebsocketserver_p.h \ + variantargument_p.h \ + signalhandler_p.h + +SOURCES += \ + qwebchannel.cpp \ + qmetaobjectpublisher.cpp \ + qwebsocketserver.cpp + +HEADERS += $$PUBLIC_HEADERS $$PRIVATE_HEADERS -- cgit v1.2.1