diff --git a/src/Network/http/CMakeLists.txt b/src/Network/http/CMakeLists.txt index 9e913d0cc..5f426515b 100644 --- a/src/Network/http/CMakeLists.txt +++ b/src/Network/http/CMakeLists.txt @@ -8,6 +8,7 @@ set(SOURCES FlightHistoryUriHandler.cxx PkgUriHandler.cxx RunUriHandler.cxx + MirrorPropertyTreeWebsocket.cxx NavdbUriHandler.cxx PropertyChangeWebsocket.cxx PropertyChangeObserver.cxx @@ -29,6 +30,7 @@ set(HEADERS Websocket.hxx PropertyChangeWebsocket.hxx PropertyChangeObserver.hxx + MirrorPropertyTreeWebsocket.hxx jsonprops.hxx SimpleDOM.hxx ) diff --git a/src/Network/http/MirrorPropertyTreeWebsocket.cxx b/src/Network/http/MirrorPropertyTreeWebsocket.cxx new file mode 100644 index 000000000..bbe6e8c2f --- /dev/null +++ b/src/Network/http/MirrorPropertyTreeWebsocket.cxx @@ -0,0 +1,381 @@ +// MirrorPropertyTreeWebsocket.cxx -- A websocket for mirroring a property sub-tree +// +// Written by James Turner, started November 2016. +// +// Copyright (C) 2016 James Turner +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License as +// published by the Free Software Foundation; either version 2 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +#include "MirrorPropertyTreeWebsocket.hxx" +#include "jsonprops.hxx" + +#include +#include + +#include +#include +#include + +#include +#include
+#include
+ +#include <3rdparty/cjson/cJSON.h> + +namespace flightgear { +namespace http { + +using std::string; + + typedef unsigned int PropertyId; // connection local property id + + struct PropertyValue + { + PropertyValue(SGPropertyNode* cur = nullptr) : + type(simgear::props::UNSPECIFIED) + { + if (!cur) { + return; + } + + type = cur->getType(); + switch (type) { + case simgear::props::INT: + intValue = cur->getIntValue(); + break; + + case simgear::props::BOOL: + intValue = cur->getBoolValue(); + break; + + case simgear::props::FLOAT: + case simgear::props::DOUBLE: + doubleValue = cur->getDoubleValue(); + break; + + case simgear::props::STRING: + case simgear::props::UNSPECIFIED: + stringValue = cur->getStringValue(); + break; + + default: + SG_LOG(SG_NETWORK, SG_INFO, "implement me!"); + break; + } + } + + bool equals(SGPropertyNode* node, const PropertyValue& other) const + { + if (other.type != type) return false; + switch (type) { + case simgear::props::INT: + case simgear::props::BOOL: + return intValue == other.intValue; + + case simgear::props::FLOAT: + case simgear::props::DOUBLE: + return std::fabs(doubleValue - other.doubleValue) < 1e-4; + + case simgear::props::STRING: + case simgear::props::UNSPECIFIED: + return stringValue == other.stringValue; + + default: + break; + } + + return false; + } + + simgear::props::Type type; + union { + int intValue; + double doubleValue; + }; + std::string stringValue; + }; + + class MirrorTreeListener : public SGPropertyChangeListener + { + public: + MirrorTreeListener() : SGPropertyChangeListener(true /* recursive */) + { + previousValues.resize(2); + } + + virtual ~MirrorTreeListener() + { + SG_LOG(SG_NETWORK, SG_INFO, "destroying MirrorTreeListener"); + } + + virtual void valueChanged(SGPropertyNode* node) override + { + auto it = idHash.find(node); + if (it == idHash.end()) { + // not new to the server, but new to the client + newNodes.insert(node); + } else { + assert(previousValues.size() > it->second); + PropertyValue newVal(node); + if (!previousValues[it->second].equals(node, newVal)) { + previousValues[it->second] = newVal; + changedNodes.insert(node); + } + } + } + + virtual void childAdded(SGPropertyNode* parent, SGPropertyNode* child) override + { + newNodes.insert(child); + } + + virtual void childRemoved(SGPropertyNode* parent, SGPropertyNode* child) override + { + removedNodes.insert(idForProperty(child)); + } + + std::set newNodes; + std::set changedNodes; + std::set removedNodes; + + PropertyId idForProperty(SGPropertyNode* prop) + { + auto it = idHash.find(prop); + if (it == idHash.end()) { + it = idHash.insert(it, std::make_pair(prop, nextPropertyId++)); + previousValues.push_back(PropertyValue(prop)); + } + return it->second; + } + + cJSON* makeJSONData() + { + cJSON* result = cJSON_CreateObject(); + + if (!newNodes.empty()) { + cJSON * newNodesArray = cJSON_CreateArray(); + + for (auto prop : newNodes) { + changedNodes.erase(prop); // avoid duplicate send + cJSON* newPropData = cJSON_CreateObject(); + cJSON_AddItemToObject(newPropData, "path", cJSON_CreateString(prop->getPath(true).c_str())); + cJSON_AddItemToObject(newPropData, "type", cJSON_CreateString(JSON::getPropertyTypeString(prop->getType()))); + cJSON_AddItemToObject(newPropData, "index", cJSON_CreateNumber(prop->getIndex())); + cJSON_AddItemToObject(newPropData, "id", cJSON_CreateNumber(idForProperty(prop))); + cJSON_AddItemToObject(newPropData, "value", JSON::valueToJson(prop)); + + cJSON_AddItemToArray(newNodesArray, newPropData); + + } + + newNodes.clear(); + cJSON_AddItemToObject(result, "created", newNodesArray); + } + + if (!changedNodes.empty()) { + cJSON * changedNodesArray = cJSON_CreateArray(); + + for (auto prop : changedNodes) { + cJSON* propData = cJSON_CreateArray(); + cJSON_AddItemToArray(propData, cJSON_CreateNumber(idForProperty(prop))); + cJSON_AddItemToArray(propData, JSON::valueToJson(prop)); + cJSON_AddItemToArray(changedNodesArray, propData); + } + + changedNodes.clear(); + cJSON_AddItemToObject(result, "changed", changedNodesArray); + } + + if (!removedNodes.empty()) { + cJSON * deletedNodesArray = cJSON_CreateArray(); + for (auto propId : removedNodes) { + cJSON_AddItemToArray(deletedNodesArray, cJSON_CreateNumber(propId)); + } + removedNodes.clear(); + cJSON_AddItemToObject(result, "removed", deletedNodesArray); + } + + return result; + } + + bool haveChangesToSend() const + { + return !newNodes.empty() || !changedNodes.empty() || !removedNodes.empty(); + } + private: + PropertyId nextPropertyId = 1; + std::unordered_map idHash; + std::vector previousValues; + }; + +#if 0 + +static void handleSetCommand(const string_list& nodes, cJSON* json, WebsocketWriter &writer) +{ + cJSON * value = cJSON_GetObjectItem(json, "value"); + if ( NULL != value ) { + if (nodes.size() > 1) { + SG_LOG(SG_NETWORK, SG_WARN, "httpd: WS set: insufficent values for nodes:" << nodes.size()); + return; + } + + SGPropertyNode_ptr n = fgGetNode(nodes.front()); + if (!n) { + SG_LOG(SG_NETWORK, SG_WARN, "httpd: set '" << nodes.front() << "' not found"); + return; + } + + setPropertyFromJson(n, value); + return; + } + + cJSON * values = cJSON_GetObjectItem(json, "values"); + if ( ( NULL == values ) || ( static_cast(cJSON_GetArraySize(values)) != nodes.size()) ) { + SG_LOG(SG_NETWORK, SG_WARN, "httpd: WS set: mismatched nodes/values sizes:" << nodes.size()); + return; + } + + string_list::const_iterator it; + int i=0; + for (it = nodes.begin(); it != nodes.end(); ++it, ++i) { + SGPropertyNode_ptr n = fgGetNode(*it); + if (!n) { + SG_LOG(SG_NETWORK, SG_WARN, "httpd: get '" << *it << "' not found"); + return; + } + + setPropertyFromJson(n, cJSON_GetArrayItem(values, i)); + } // of nodes iteration +} + +static void handleExecCommand(cJSON* json) +{ + cJSON* name = cJSON_GetObjectItem(json, "fgcommand"); + if ((NULL == name )|| (NULL == name->valuestring)) { + SG_LOG(SG_NETWORK, SG_WARN, "httpd: exec: no fgcommand name"); + return; + } + + SGPropertyNode_ptr arg(new SGPropertyNode); + JSON::addChildrenToProp( json, arg ); + + globals->get_commands()->execute(name->valuestring, arg); +} +#endif + +MirrorPropertyTreeWebsocket::MirrorPropertyTreeWebsocket(const std::string& path) : + _listener(new MirrorTreeListener), + _minSendInterval(100) +{ + _subtreeRoot = globals->get_props()->getNode(path, true); + _subtreeRoot->addChangeListener(_listener.get()); + _subtreeRoot->fireCreatedRecursive(); +} + +MirrorPropertyTreeWebsocket::~MirrorPropertyTreeWebsocket() +{ + SG_LOG(SG_NETWORK, SG_INFO, "shutting down MirrorPropertyTreeWebsocket"); +} + +void MirrorPropertyTreeWebsocket::close() +{ + _subtreeRoot->removeChangeListener(_listener.get()); + + #if 0 + SG_LOG(SG_NETWORK, SG_INFO, "closing PropertyChangeWebsocket #" << id); + _watchedNodes.clear(); + #endif +} + +void MirrorPropertyTreeWebsocket::handleRequest(const HTTPRequest & request, WebsocketWriter &writer) +{ + if (request.Content.empty()) return; +#if 0 + /* + * allowed JSON is + { + command : 'addListener', + nodes : [ + '/bar/baz', + '/foo/bar' + ], + node: '/bax/foo' + } + */ + cJSON * json = cJSON_Parse(request.Content.c_str()); + if ( NULL != json) { + string command; + cJSON * j = cJSON_GetObjectItem(json, "command"); + if ( NULL != j && NULL != j->valuestring) { + command = j->valuestring; + } + + // handle a single node name, or an array of them + string_list nodeNames; + j = cJSON_GetObjectItem(json, "node"); + if ( NULL != j && NULL != j->valuestring) { + nodeNames.push_back(simgear::strutils::strip(string(j->valuestring))); + } + + cJSON * nodes = cJSON_GetObjectItem(json, "nodes"); + if ( NULL != nodes) { + for (int i = 0; i < cJSON_GetArraySize(nodes); i++) { + cJSON * node = cJSON_GetArrayItem(nodes, i); + if ( NULL == node) continue; + if ( NULL == node->valuestring) continue; + nodeNames.push_back(simgear::strutils::strip(string(node->valuestring))); + } + } + + if (command == "get") { + handleGetCommand(nodeNames, writer); + } else if (command == "set") { + handleSetCommand(nodeNames, json, writer); + } else if (command == "exec") { + handleExecCommand(json); + } else { + string_list::const_iterator it; + for (it = nodeNames.begin(); it != nodeNames.end(); ++it) { + _watchedNodes.handleCommand(command, *it, _propertyChangeObserver); + } + } + + cJSON_Delete(json); + } + #endif +} + +void MirrorPropertyTreeWebsocket::poll(WebsocketWriter & writer) +{ + if (!_listener->haveChangesToSend()) { + return; + } + + if (_lastSendTime.elapsedMSec() < _minSendInterval) { + return; + } + + // okay, we will send now, update the send stamp + _lastSendTime.stamp(); + + cJSON * json = _listener->makeJSONData(); + char * jsonString = cJSON_PrintUnformatted( json ); + writer.writeText( jsonString ); + free( jsonString ); + cJSON_Delete( json ); +} + +} // namespace http +} // namespace flightgear diff --git a/src/Network/http/MirrorPropertyTreeWebsocket.hxx b/src/Network/http/MirrorPropertyTreeWebsocket.hxx new file mode 100644 index 000000000..357000046 --- /dev/null +++ b/src/Network/http/MirrorPropertyTreeWebsocket.hxx @@ -0,0 +1,58 @@ +// MirrorPropertyTreeWebsocket.hxx -- A websocket for mirroring a property sub-tree +// +// Written by James Turner, started November 2016. +// +// Copyright (C) 2016 James Turner +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License as +// published by the Free Software Foundation; either version 2 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +#ifndef MIRROR_PROP_TREE_WEBSOCKET_HXX_ +#define MIRROR_PROP_TREE_WEBSOCKET_HXX_ + +#include "Websocket.hxx" + +#include +#include + +#include + +namespace flightgear { +namespace http { + + class MirrorTreeListener; + +class MirrorPropertyTreeWebsocket : public Websocket +{ +public: + MirrorPropertyTreeWebsocket(const std::string& path); + virtual ~MirrorPropertyTreeWebsocket(); + + virtual void close(); + virtual void handleRequest(const HTTPRequest & request, WebsocketWriter & writer); + virtual void poll(WebsocketWriter & writer); + +private: + friend class MirrorTreeListener; + + SGPropertyNode_ptr _subtreeRoot; + std::unique_ptr _listener; + unsigned int _minSendInterval; + SGTimeStamp _lastSendTime; +}; + +} +} + +#endif /* MIRROR_PROP_TREE_WEBSOCKET_HXX_ */ diff --git a/src/Network/http/httpd.cxx b/src/Network/http/httpd.cxx index 358b247d1..ef179283e 100644 --- a/src/Network/http/httpd.cxx +++ b/src/Network/http/httpd.cxx @@ -21,6 +21,7 @@ #include "httpd.hxx" #include "HTTPRequest.hxx" #include "PropertyChangeWebsocket.hxx" +#include "MirrorPropertyTreeWebsocket.hxx" #include "ScreenshotUriHandler.hxx" #include "PropertyUriHandler.hxx" #include "JsonUriHandler.hxx" @@ -587,6 +588,9 @@ Websocket * MongooseHttpd::newWebsocket(const string & uri) if (uri.find("/PropertyListener") == 0) { SG_LOG(SG_NETWORK, SG_INFO, "new PropertyChangeWebsocket for: " << uri); return new PropertyChangeWebsocket(&_propertyChangeObserver); + } else if (uri.find("/PropertyTreeMirror/") == 0) { + SG_LOG(SG_NETWORK, SG_INFO, "new MirrorPropertyTreeWebsocket for: " << uri); + return new MirrorPropertyTreeWebsocket(uri.substr(20)); } return NULL; } diff --git a/src/Network/http/jsonprops.cxx b/src/Network/http/jsonprops.cxx index 25dc1fbe6..140d6c84b 100644 --- a/src/Network/http/jsonprops.cxx +++ b/src/Network/http/jsonprops.cxx @@ -26,7 +26,7 @@ namespace http { using std::string; -static const char * getPropertyTypeString(simgear::props::Type type) +const char * JSON::getPropertyTypeString(simgear::props::Type type) { switch (type) { case simgear::props::NONE: @@ -70,6 +70,28 @@ static const char * getPropertyTypeString(simgear::props::Type type) } } +cJSON * JSON::valueToJson(SGPropertyNode_ptr n) +{ + if( !n->hasValue() ) + return cJSON_CreateNull(); + + switch( n->getType() ) { + case simgear::props::BOOL: + return cJSON_CreateBool(n->getBoolValue()); + case simgear::props::INT: + case simgear::props::LONG: + case simgear::props::FLOAT: + case simgear::props::DOUBLE: { + double val = n->getDoubleValue(); + return SGMiscd::isNaN(val) ? cJSON_CreateNull() : cJSON_CreateNumber(val); + } + + default: + return cJSON_CreateString(n->getStringValue()); + } +} + + cJSON * JSON::toJson(SGPropertyNode_ptr n, int depth, double timestamp ) { cJSON * json = cJSON_CreateObject(); diff --git a/src/Network/http/jsonprops.hxx b/src/Network/http/jsonprops.hxx index 4f2fcf67f..dfabd1fad 100644 --- a/src/Network/http/jsonprops.hxx +++ b/src/Network/http/jsonprops.hxx @@ -27,11 +27,15 @@ namespace flightgear { namespace http { + class JSON { public: static cJSON * toJson(SGPropertyNode_ptr n, int depth, double timestamp = -1.0 ); static std::string toJsonString(bool indent, SGPropertyNode_ptr n, int depth, double timestamp = -1.0 ); - + + static const char * getPropertyTypeString(simgear::props::Type type); + static cJSON * valueToJson(SGPropertyNode_ptr n); + static void toProp(cJSON * json, SGPropertyNode_ptr base); static void addChildrenToProp(cJSON * json, SGPropertyNode_ptr base); };