diff --git a/assignment-client/CMakeLists.txt b/assignment-client/CMakeLists.txt index 315eeb6b83..dc20763953 100644 --- a/assignment-client/CMakeLists.txt +++ b/assignment-client/CMakeLists.txt @@ -1,6 +1,6 @@ set(TARGET_NAME assignment-client) -setup_hifi_project(Core Gui Network Script Widgets) +setup_hifi_project(Core Gui Network Script Widgets WebSockets) add_dependency_external_projects(glm) find_package(GLM REQUIRED) diff --git a/assignment-client/src/Agent.cpp b/assignment-client/src/Agent.cpp index d656464c10..300976f81c 100644 --- a/assignment-client/src/Agent.cpp +++ b/assignment-client/src/Agent.cpp @@ -24,6 +24,7 @@ #include #include +#include #include // TODO: consider moving to scriptengine.h #include "avatars/ScriptableAvatar.h" @@ -180,10 +181,17 @@ void Agent::run() { // register ourselves to the script engine _scriptEngine.registerGlobalObject("Agent", this); + if (!_payload.isEmpty()) { + _scriptEngine.setParentURL(_payload); + } + _scriptEngine.init(); // must be done before we set up the viewers _scriptEngine.registerGlobalObject("SoundCache", DependencyManager::get().data()); + QScriptValue webSocketServerConstructorValue = _scriptEngine.newFunction(WebSocketServerClass::constructor); + _scriptEngine.globalObject().setProperty("WebSocketServer", webSocketServerConstructorValue); + auto entityScriptingInterface = DependencyManager::get(); _scriptEngine.registerGlobalObject("EntityViewer", &_entityViewer); diff --git a/examples/libraries/unitTest.js b/examples/libraries/unitTest.js index beb3387898..7d5234933f 100644 --- a/examples/libraries/unitTest.js +++ b/examples/libraries/unitTest.js @@ -11,17 +11,14 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -var test = function(name, func) { +test = function(name, func, timeout) { print("Running test: " + name); - - var unitTest = new UnitTest(name, func); - - try { - unitTest.run(); + var unitTest = new UnitTest(name, func, timeout); + unitTest.run(function(unitTest) { print(" Success: " + unitTest.numAssertions + " assertions passed"); - } catch (error) { + }, function(unitTest, error) { print(" Failure: " + error.name + " " + error.message); - } + }); }; AssertionException = function(expected, actual, message) { @@ -36,13 +33,86 @@ UnthrownException = function(message) { this.name = 'UnthrownException'; }; -UnitTest = function(name, func) { - this.numAssertions = 0; - this.func = func; +TimeoutException = function() { + print("Creating exception"); + this.message = "UnitTest timed out\n"; + this.name = 'TimeoutException'; }; -UnitTest.prototype.run = function() { - this.func(); +SequentialUnitTester = function() { + this.tests = []; + this.testIndex = -1; +}; + +SequentialUnitTester.prototype.addTest = function(name, func, timeout) { + var _this = this; + this.tests.push(function() { + print("Running test: " + name); + var unitTest = new UnitTest(name, func, timeout); + unitTest.run(function(unitTest) { + print(" Success: " + unitTest.numAssertions + " assertions passed"); + _this._nextTest(); + }, function(unitTest, error) { + print(" Failure: " + error.name + " " + error.message); + _this._nextTest(); + }); + }); +}; + +SequentialUnitTester.prototype._nextTest = function() { + this.testIndex++; + if (this.testIndex < this.tests.length) { + this.tests[this.testIndex](); + return; + } + print("Completed all UnitTests"); +}; + +SequentialUnitTester.prototype.run = function() { + this._nextTest(); +}; + +UnitTest = function(name, func, timeout) { + this.numAssertions = 0; + this.func = func; + this.timeout = timeout; +}; + +UnitTest.prototype.run = function(successCallback, failureCallback) { + var _this = this; + this.successCallback = successCallback; + this.failureCallback = failureCallback; + if (this.timeout !== undefined) { + this.timeoutTimer = Script.setTimeout(function() { + _this.failureCallback(this, new TimeoutException()); + }, this.timeout); + } + try { + this.func(); + if (this.timeout === undefined) { + successCallback(this); + } + } catch (exception) { + this.handleException(exception); + } +}; + +UnitTest.prototype.registerCallbackFunction = function(func) { + var _this = this; + return function(one, two, three, four, five, six) { + try { + func(one, two, three, four, five, six); + } catch (exception) { + _this.handleException(exception); + } + }; +}; + +UnitTest.prototype.handleException = function(exception) { + if (this.timeout !== undefined) { + Script.clearTimeout(this.timeoutTimer); + } + this.failureCallback(this, exception); }; UnitTest.prototype.assertNotEquals = function(expected, actual, message) { @@ -83,7 +153,7 @@ UnitTest.prototype.assertNull = function(value, message) { UnitTest.prototype.arrayEqual = function(array1, array2, message) { this.numAssertions++; if (array1.length !== array2.length) { - throw new AssertionException(array1.length , array2.length , message); + throw new AssertionException(array1.length, array2.length , message); } for (var i = 0; i < array1.length; ++i) { if (array1[i] !== array2[i]) { @@ -101,4 +171,11 @@ UnitTest.prototype.raises = function(func, message) { } throw new UnthrownException(message); -} \ No newline at end of file +} + +UnitTest.prototype.done = function() { + if (this.timeout !== undefined) { + Script.clearTimeout(this.timeoutTimer); + this.successCallback(this); + } +} diff --git a/examples/utilities/diagnostics/testWebSocket.js b/examples/utilities/diagnostics/testWebSocket.js new file mode 100644 index 0000000000..c356fd99a8 --- /dev/null +++ b/examples/utilities/diagnostics/testWebSocket.js @@ -0,0 +1,102 @@ +// +// testWebSocket.js +// examples +// +// Created by Thijs Wenker on 8/18/15 +// Copyright 2015 High Fidelity, Inc. +// +// WebSocket and WebSocketServer Tests +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + +Script.include("../../libraries/unitTest.js"); + +// We set the unit testing timeout to 1000 milliseconds by default. Please increase if the test fails due to a slow connection. +const UNITTEST_TIMEOUT = 1000; +const WEBSOCKET_PING_URL = "ws://echo.websocket.org"; +// Please do not register the following domain + gTLD: +const WEBSOCKET_INVALID_URL = "ws://thisisnotavaliddomainname.invalid"; +const TEST_MESSAGE = "This is a test message."; + +var unitTests = new SequentialUnitTester(); + +unitTests.addTest("Test default WebSocket values", function(finished) { + var _this = this; + var webSocket = new WebSocket(WEBSOCKET_PING_URL); + + webSocket.onmessage = this.registerCallbackFunction(function(event) { + _this.assertEquals(TEST_MESSAGE, event.data, "event.data should be '" + TEST_MESSAGE + "'"); + webSocket.close(); + }); + webSocket.onopen = this.registerCallbackFunction(function(event) { + _this.assertEquals(webSocket.OPEN, webSocket.readyState, "readyState should be OPEN"); + webSocket.send(TEST_MESSAGE); + }); + webSocket.onclose = this.registerCallbackFunction(function(event) { + _this.assertEquals(webSocket.CLOSED, webSocket.readyState, "readyState should be CLOSED"); + _this.done(); + }); + this.assertEquals(webSocket.CONNECTING, webSocket.readyState, "readyState should be CONNECTING"); + this.assertEquals("blob", webSocket.binaryType, "binaryType should be 'blob'"); + this.assertEquals(0, webSocket.bufferedAmount, "bufferedAmount should be 0"); + this.assertEquals("", webSocket.extensions, "extensions should be an empty string by default"); + this.assertEquals("", webSocket.protocol, "protocol should be an empty string by default"); + this.assertEquals(WEBSOCKET_PING_URL, webSocket.url, "url should be '" + WEBSOCKET_PING_URL + "'"); +}, UNITTEST_TIMEOUT); + +unitTests.addTest("Test WebSocket invalid URL", function(finished) { + var _this = this; + var webSocket = new WebSocket(WEBSOCKET_INVALID_URL); + var hadError = false; + webSocket.onerror = this.registerCallbackFunction(function() { + hadError = true; + _this.done(); + }); + webSocket.onclose = this.registerCallbackFunction(function(event) { + _this.assertEquals(webSocket.CLOSED, webSocket.readyState, "readyState should be CLOSED"); + }); + this.assertEquals(webSocket.CONNECTING, webSocket.readyState, "readyState should be CONNECTING"); + this.assertEquals(WEBSOCKET_INVALID_URL, webSocket.url, "url should be '" + WEBSOCKET_INVALID_URL + "'"); +}, UNITTEST_TIMEOUT); + +if (this.WebSocketServer === undefined) { + print("Skipping WebSocketServer tests."); +} else { + unitTests.addTest("Test WebSocketServer with three clients", function(finished) { + var _this = this; + const NUMBER_OF_CLIENTS = 3; + var connectedClients = 0; + var respondedClients = 0; + var webSocketServer = new WebSocketServer(); + _this.assertEquals(true, webSocketServer.listening, "listening should be true"); + webSocketServer.newConnection.connect(this.registerCallbackFunction(function(newClient) { + connectedClients++; + newClient.onmessage = _this.registerCallbackFunction(function(event) { + var data = JSON.parse(event.data); + _this.assertEquals(TEST_MESSAGE, data.message, "data.message should be '" + TEST_MESSAGE + "'"); + respondedClients++; + if (respondedClients === NUMBER_OF_CLIENTS) { + webSocketServer.close(); + _this.assertEquals(false, webSocketServer.listening, "listening should be false"); + _this.done(); + } + }); + newClient.send(JSON.stringify({message: TEST_MESSAGE, client: connectedClients})); + })); + var newSocket1 = new WebSocket(webSocketServer.url); + newSocket1.onmessage = this.registerCallbackFunction(function(event) { + newSocket1.send(event.data); + }); + var newSocket2 = new WebSocket(webSocketServer.url); + newSocket2.onmessage = this.registerCallbackFunction(function(event) { + newSocket2.send(event.data); + }); + var newSocket3 = new WebSocket(webSocketServer.url); + newSocket3.onmessage = this.registerCallbackFunction(function(event) { + newSocket3.send(event.data); + }); + }, UNITTEST_TIMEOUT); +} + +unitTests.run(); diff --git a/interface/CMakeLists.txt b/interface/CMakeLists.txt index d858673774..0c531dfacb 100644 --- a/interface/CMakeLists.txt +++ b/interface/CMakeLists.txt @@ -40,7 +40,7 @@ else () list(REMOVE_ITEM INTERFACE_SRCS ${SPEECHRECOGNIZER_CPP}) endif () -find_package(Qt5 COMPONENTS Gui Multimedia Network OpenGL Qml Quick Script Svg WebKitWidgets) +find_package(Qt5 COMPONENTS Gui Multimedia Network OpenGL Qml Quick Script Svg WebKitWidgets WebSockets) # grab the ui files in resources/ui file (GLOB_RECURSE QT_UI_FILES ui/*.ui) diff --git a/libraries/script-engine/CMakeLists.txt b/libraries/script-engine/CMakeLists.txt index 6bb53389af..670a13f081 100644 --- a/libraries/script-engine/CMakeLists.txt +++ b/libraries/script-engine/CMakeLists.txt @@ -3,7 +3,7 @@ set(TARGET_NAME script-engine) setup_memory_debugger() # use setup_hifi_library macro to setup our project and link appropriate Qt modules -setup_hifi_library(Gui Network Script Widgets) +setup_hifi_library(Gui Network Script WebSockets Widgets) add_dependency_external_projects(glm) find_package(GLM REQUIRED) diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index cfd6cda56b..b42daa710a 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -38,6 +38,7 @@ #include "ScriptEngine.h" #include "TypedArrays.h" #include "XMLHttpRequestClass.h" +#include "WebSocketClass.h" #include "SceneScriptingInterface.h" @@ -343,6 +344,9 @@ void ScriptEngine::init() { QScriptValue xmlHttpRequestConstructorValue = newFunction(XMLHttpRequestClass::constructor); globalObject().setProperty("XMLHttpRequest", xmlHttpRequestConstructorValue); + QScriptValue webSocketConstructorValue = newFunction(WebSocketClass::constructor); + globalObject().setProperty("WebSocket", webSocketConstructorValue); + QScriptValue printConstructorValue = newFunction(debugPrint); globalObject().setProperty("print", printConstructorValue); @@ -353,6 +357,9 @@ void ScriptEngine::init() { qScriptRegisterMetaType(this, inputControllerToScriptValue, inputControllerFromScriptValue); qScriptRegisterMetaType(this, avatarDataToScriptValue, avatarDataFromScriptValue); qScriptRegisterMetaType(this, animationDetailsToScriptValue, animationDetailsFromScriptValue); + qScriptRegisterMetaType(this, webSocketToScriptValue, webSocketFromScriptValue); + qScriptRegisterMetaType(this, qWSCloseCodeToScriptValue, qWSCloseCodeFromScriptValue); + qScriptRegisterMetaType(this, wscReadyStateToScriptValue, wscReadyStateFromScriptValue); registerGlobalObject("Script", this); registerGlobalObject("Audio", &AudioScriptingInterface::getInstance()); diff --git a/libraries/script-engine/src/WebSocketClass.cpp b/libraries/script-engine/src/WebSocketClass.cpp new file mode 100644 index 0000000000..c844dc3582 --- /dev/null +++ b/libraries/script-engine/src/WebSocketClass.cpp @@ -0,0 +1,127 @@ +// +// WebSocketClass.cpp +// libraries/script-engine/src/ +// +// Created by Thijs Wenker on 8/4/15. +// Copyright (c) 2015 High Fidelity, Inc. All rights reserved. +// +// This class is an implementation of the WebSocket object for scripting use. It provides a near-complete implementation +// of the class described in the Mozilla docs: https://developer.mozilla.org/en-US/docs/Web/API/WebSocket +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "ScriptEngine.h" +#include "WebSocketClass.h" + +WebSocketClass::WebSocketClass(QScriptEngine* engine, QString url) : + _engine(engine), + _webSocket(new QWebSocket()) +{ + initialize(); + _webSocket->open(url); +} + +WebSocketClass::WebSocketClass(QScriptEngine* engine, QWebSocket* qWebSocket) : + _engine(engine), + _webSocket(qWebSocket) +{ + initialize(); +} + +void WebSocketClass::initialize() { + connect(_webSocket, &QWebSocket::disconnected, this, &WebSocketClass::handleOnClose); + connect(_webSocket, &QWebSocket::textMessageReceived, this, &WebSocketClass::handleOnMessage); + connect(_webSocket, &QWebSocket::connected, this, &WebSocketClass::handleOnOpen); + connect(_webSocket, static_cast(&QWebSocket::error), this, + &WebSocketClass::handleOnError); + _binaryType = QStringLiteral("blob"); +} + +QScriptValue WebSocketClass::constructor(QScriptContext* context, QScriptEngine* engine) { + QString url; + if (context->argumentCount() > 0) { + url = context->argument(0).toString(); + } + return engine->newQObject(new WebSocketClass(engine, url), QScriptEngine::ScriptOwnership); +} + +WebSocketClass::~WebSocketClass() { + _webSocket->deleteLater(); +} + +void WebSocketClass::send(QScriptValue message) { + _webSocket->sendTextMessage(message.toString()); +} + +void WebSocketClass::close() { + this->close(QWebSocketProtocol::CloseCodeNormal); +} + +void WebSocketClass::close(QWebSocketProtocol::CloseCode closeCode) { + this->close(closeCode, QStringLiteral("")); +} + +void WebSocketClass::close(QWebSocketProtocol::CloseCode closeCode, QString reason) { + _webSocket->close(closeCode, reason); +} + +void WebSocketClass::handleOnClose() { + bool hasError = (_webSocket->error() != QAbstractSocket::UnknownSocketError); + if (_onCloseEvent.isFunction()) { + QScriptValueList args; + QScriptValue arg = _engine->newObject(); + arg.setProperty("code", hasError ? QWebSocketProtocol::CloseCodeAbnormalDisconnection : _webSocket->closeCode()); + arg.setProperty("reason", _webSocket->closeReason()); + arg.setProperty("wasClean", !hasError); + args << arg; + _onCloseEvent.call(QScriptValue(), args); + } +} + +void WebSocketClass::handleOnError(QAbstractSocket::SocketError error) { + if (_onErrorEvent.isFunction()) { + _onErrorEvent.call(); + } +} + +void WebSocketClass::handleOnMessage(const QString& message) { + if (_onMessageEvent.isFunction()) { + QScriptValueList args; + QScriptValue arg = _engine->newObject(); + arg.setProperty("data", message); + args << arg; + _onMessageEvent.call(QScriptValue(), args); + } +} + +void WebSocketClass::handleOnOpen() { + if (_onOpenEvent.isFunction()) { + _onOpenEvent.call(); + } +} + +QScriptValue qWSCloseCodeToScriptValue(QScriptEngine* engine, const QWebSocketProtocol::CloseCode &closeCode) { + return closeCode; +} + +void qWSCloseCodeFromScriptValue(const QScriptValue &object, QWebSocketProtocol::CloseCode &closeCode) { + closeCode = (QWebSocketProtocol::CloseCode)object.toUInt16(); +} + +QScriptValue webSocketToScriptValue(QScriptEngine* engine, WebSocketClass* const &in) { + return engine->newQObject(in, QScriptEngine::ScriptOwnership); +} + +void webSocketFromScriptValue(const QScriptValue &object, WebSocketClass* &out) { + out = qobject_cast(object.toQObject()); +} + +QScriptValue wscReadyStateToScriptValue(QScriptEngine* engine, const WebSocketClass::ReadyState& readyState) { + return readyState; +} + +void wscReadyStateFromScriptValue(const QScriptValue& object, WebSocketClass::ReadyState& readyState) { + readyState = (WebSocketClass::ReadyState)object.toUInt16(); +} diff --git a/libraries/script-engine/src/WebSocketClass.h b/libraries/script-engine/src/WebSocketClass.h new file mode 100644 index 0000000000..8ba8ecf362 --- /dev/null +++ b/libraries/script-engine/src/WebSocketClass.h @@ -0,0 +1,140 @@ +// +// WebSocketClass.h +// libraries/script-engine/src/ +// +// Created by Thijs Wenker on 8/4/15. +// Copyright (c) 2015 High Fidelity, Inc. All rights reserved. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_WebSocketClass_h +#define hifi_WebSocketClass_h + +#include +#include +#include + +class WebSocketClass : public QObject { + Q_OBJECT + Q_PROPERTY(QString binaryType READ getBinaryType WRITE setBinaryType) + Q_PROPERTY(ulong bufferedAmount READ getBufferedAmount) + Q_PROPERTY(QString extensions READ getExtensions) + + Q_PROPERTY(QScriptValue onclose READ getOnClose WRITE setOnClose) + Q_PROPERTY(QScriptValue onerror READ getOnError WRITE setOnError) + Q_PROPERTY(QScriptValue onmessage READ getOnMessage WRITE setOnMessage) + Q_PROPERTY(QScriptValue onopen READ getOnOpen WRITE setOnOpen) + + Q_PROPERTY(QString protocol READ getProtocol) + Q_PROPERTY(WebSocketClass::ReadyState readyState READ getReadyState) + Q_PROPERTY(QString url READ getURL) + + Q_PROPERTY(WebSocketClass::ReadyState CONNECTING READ getConnecting CONSTANT) + Q_PROPERTY(WebSocketClass::ReadyState OPEN READ getOpen CONSTANT) + Q_PROPERTY(WebSocketClass::ReadyState CLOSING READ getClosing CONSTANT) + Q_PROPERTY(WebSocketClass::ReadyState CLOSED READ getClosed CONSTANT) + +public: + WebSocketClass(QScriptEngine* engine, QString url); + WebSocketClass(QScriptEngine* engine, QWebSocket* qWebSocket); + ~WebSocketClass(); + + static QScriptValue constructor(QScriptContext* context, QScriptEngine* engine); + + enum ReadyState { + CONNECTING = 0, + OPEN, + CLOSING, + CLOSED + }; + + QWebSocket* getWebSocket() { return _webSocket; } + + ReadyState getConnecting() const { return CONNECTING; }; + ReadyState getOpen() const { return OPEN; }; + ReadyState getClosing() const { return CLOSING; }; + ReadyState getClosed() const { return CLOSED; }; + + void setBinaryType(QString binaryType) { _binaryType = binaryType; } + QString getBinaryType() { return _binaryType; } + + // extensions is a empty string until supported in QT WebSockets + QString getExtensions() { return QString(); } + + // protocol is a empty string until supported in QT WebSockets + QString getProtocol() { return QString(); } + + ulong getBufferedAmount() { return 0; } + + QString getURL() { return _webSocket->requestUrl().toDisplayString(); } + + ReadyState getReadyState() { + switch (_webSocket->state()) { + case QAbstractSocket::SocketState::HostLookupState: + case QAbstractSocket::SocketState::ConnectingState: + return CONNECTING; + case QAbstractSocket::SocketState::ConnectedState: + case QAbstractSocket::SocketState::BoundState: + case QAbstractSocket::SocketState::ListeningState: + return OPEN; + case QAbstractSocket::SocketState::ClosingState: + return CLOSING; + } + return CLOSED; + } + + void setOnClose(QScriptValue eventFunction) { _onCloseEvent = eventFunction; } + QScriptValue getOnClose() { return _onCloseEvent; } + + void setOnError(QScriptValue eventFunction) { _onErrorEvent = eventFunction; } + QScriptValue getOnError() { return _onErrorEvent; } + + void setOnMessage(QScriptValue eventFunction) { _onMessageEvent = eventFunction; } + QScriptValue getOnMessage() { return _onMessageEvent; } + + void setOnOpen(QScriptValue eventFunction) { _onOpenEvent = eventFunction; } + QScriptValue getOnOpen() { return _onOpenEvent; } + +public slots: + void send(QScriptValue message); + + void close(); + void close(QWebSocketProtocol::CloseCode closeCode); + void close(QWebSocketProtocol::CloseCode closeCode, QString reason); + +private: + QWebSocket* _webSocket; + QScriptEngine* _engine; + + QScriptValue _onCloseEvent; + QScriptValue _onErrorEvent; + QScriptValue _onMessageEvent; + QScriptValue _onOpenEvent; + + QString _binaryType; + + void initialize(); + +private slots: + void handleOnClose(); + void handleOnError(QAbstractSocket::SocketError error); + void handleOnMessage(const QString& message); + void handleOnOpen(); + +}; + +Q_DECLARE_METATYPE(QWebSocketProtocol::CloseCode); +Q_DECLARE_METATYPE(WebSocketClass::ReadyState); + +QScriptValue qWSCloseCodeToScriptValue(QScriptEngine* engine, const QWebSocketProtocol::CloseCode& closeCode); +void qWSCloseCodeFromScriptValue(const QScriptValue& object, QWebSocketProtocol::CloseCode& closeCode); + +QScriptValue webSocketToScriptValue(QScriptEngine* engine, WebSocketClass* const &in); +void webSocketFromScriptValue(const QScriptValue &object, WebSocketClass* &out); + +QScriptValue wscReadyStateToScriptValue(QScriptEngine* engine, const WebSocketClass::ReadyState& readyState); +void wscReadyStateFromScriptValue(const QScriptValue& object, WebSocketClass::ReadyState& readyState); + +#endif // hifi_WebSocketClass_h diff --git a/libraries/script-engine/src/WebSocketServerClass.cpp b/libraries/script-engine/src/WebSocketServerClass.cpp new file mode 100644 index 0000000000..3b3a02a7c9 --- /dev/null +++ b/libraries/script-engine/src/WebSocketServerClass.cpp @@ -0,0 +1,69 @@ +// +// WebSocketServerClass.cpp +// libraries/script-engine/src/ +// +// Created by Thijs Wenker on 8/10/15. +// Copyright (c) 2015 High Fidelity, Inc. All rights reserved. +// +// Making WebSocketServer accessible through scripting. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "ScriptEngine.h" +#include "WebSocketServerClass.h" + +WebSocketServerClass::WebSocketServerClass(QScriptEngine* engine, const QString& serverName, const quint16 port) : + _engine(engine), + _webSocketServer(serverName, QWebSocketServer::SslMode::NonSecureMode) +{ + if (_webSocketServer.listen(QHostAddress::Any, port)) { + connect(&_webSocketServer, &QWebSocketServer::newConnection, this, &WebSocketServerClass::onNewConnection); + } +} + +QScriptValue WebSocketServerClass::constructor(QScriptContext* context, QScriptEngine* engine) { + // the serverName is used in handshakes + QString serverName = QStringLiteral("HighFidelity - Scripted WebSocket Listener"); + // port 0 will auto-assign a free port + quint16 port = 0; + QScriptValue callee = context->callee(); + if (context->argumentCount() > 0) { + QScriptValue options = context->argument(0); + QScriptValue portOption = options.property(QStringLiteral("port")); + if (portOption.isValid() && portOption.isNumber()) { + port = portOption.toNumber(); + } + QScriptValue serverNameOption = options.property(QStringLiteral("serverName")); + if (serverNameOption.isValid() && serverNameOption.isString()) { + serverName = serverNameOption.toString(); + } + } + return engine->newQObject(new WebSocketServerClass(engine, serverName, port), QScriptEngine::ScriptOwnership); +} + +WebSocketServerClass::~WebSocketServerClass() { + if (_webSocketServer.isListening()) { + close(); + } + _clients.empty(); +} + +void WebSocketServerClass::onNewConnection() { + WebSocketClass* newClient = new WebSocketClass(_engine, _webSocketServer.nextPendingConnection()); + _clients << newClient; + connect(newClient->getWebSocket(), &QWebSocket::disconnected, [newClient, this]() { + _clients.removeOne(newClient); + }); + emit newConnection(newClient); +} + +void WebSocketServerClass::close() { + foreach(WebSocketClass* client, _clients) { + if (client->getReadyState() != WebSocketClass::ReadyState::CLOSED) { + client->close(QWebSocketProtocol::CloseCode::CloseCodeGoingAway, "Server closing."); + } + } + _webSocketServer.close(); +} diff --git a/libraries/script-engine/src/WebSocketServerClass.h b/libraries/script-engine/src/WebSocketServerClass.h new file mode 100644 index 0000000000..972bf9c032 --- /dev/null +++ b/libraries/script-engine/src/WebSocketServerClass.h @@ -0,0 +1,52 @@ +// +// WebSocketServerClass.h +// libraries/script-engine/src/ +// +// Created by Thijs Wenker on 8/10/15. +// Copyright (c) 2015 High Fidelity, Inc. All rights reserved. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_WebSocketServerClass_h +#define hifi_WebSocketServerClass_h + +#include +#include +#include +#include "WebSocketClass.h" + +class WebSocketServerClass : public QObject { + Q_OBJECT + Q_PROPERTY(QString url READ getURL) + Q_PROPERTY(quint16 port READ getPort) + Q_PROPERTY(bool listening READ isListening) + +public: + WebSocketServerClass(QScriptEngine* engine, const QString& serverName, const quint16 port); + ~WebSocketServerClass(); + + QString getURL() { return _webSocketServer.serverUrl().toDisplayString(); } + quint16 getPort() { return _webSocketServer.serverPort(); } + bool isListening() { return _webSocketServer.isListening(); } + + static QScriptValue constructor(QScriptContext* context, QScriptEngine* engine); + +public slots: + void close(); + +private: + QWebSocketServer _webSocketServer; + QScriptEngine* _engine; + QList _clients; + +private slots: + void onNewConnection(); + +signals: + void newConnection(WebSocketClass* client); + +}; + +#endif // hifi_WebSocketServerClass_h