1
0
Fork 0
flightgear/src/Network/http/MirrorPropertyTreeWebsocket.cxx
2017-11-13 15:58:08 +01:00

464 lines
15 KiB
C++

// 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 <algorithm>
#include <unordered_map>
#include <set>
#include <simgear/debug/logstream.hxx>
#include <simgear/props/props.hxx>
#include <simgear/structure/commands.hxx>
#include <simgear/props/props_io.hxx>
#include <Main/globals.hxx>
#include <Main/fg_props.hxx>
#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::NONE)
{
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;
case simgear::props::NONE:
break;
default:
SG_LOG(SG_NETWORK, SG_INFO, "implement me!" << type);
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;
case simgear::props::NONE:
return true;
default:
break;
}
return false;
}
simgear::props::Type type;
union {
int intValue;
double doubleValue;
};
std::string stringValue;
};
struct RecentlyRemovedNode
{
RecentlyRemovedNode() : type(simgear::props::NONE), id(0) { }
RecentlyRemovedNode(SGPropertyNode* node, unsigned int aId) :
type(node->getType()),
path(node->getPath()),
id(aId)
{}
simgear::props::Type type;
std::string path;
unsigned int id;
bool operator==(const RecentlyRemovedNode& other) const
{
return (type == other.type) && (path == other.path);
}
};
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
{
// this works becuase custom operator== overload on RecentlyRemovedNode
// ignores the ID value.
RecentlyRemovedNode m(child, 0);
auto rrIt = std::find(recentlyRemoved.begin(), recentlyRemoved.end(), m);
if (rrIt != recentlyRemoved.end()) {
// recycle nodes which get thrashed from Nasal (deleted + re-created
// each time a Nasal timer fires)
removedNodes.erase(rrIt->id); // don't remove it!
idHash.insert(std::make_pair(child, rrIt->id));
changedNodes.insert(child);
recentlyRemoved.erase(rrIt);
return;
}
newNodes.insert(child);
}
virtual void childRemoved(SGPropertyNode* parent, SGPropertyNode* child) override
{
changedNodes.erase(child); // have to do this here with the pointer valid
newNodes.erase(child);
auto it = idHash.find(child);
if (it != idHash.end()) {
removedNodes.insert(it->second);
idHash.erase(it);
// record so we can map removed+add of the same property into
// a simple value change (this happens commonly with the canvas
// due to lazy Nasal scripting)
recentlyRemoved.push_back(RecentlyRemovedNode(child, it->second));
}
}
void registerSubtree(SGPropertyNode* node)
{
valueChanged(node);
// and recurse
int child = 0;
for (; child < node->nChildren(); ++child) {
registerSubtree(node->getChild(child));
}
}
std::set<SGPropertyNode*> newNodes;
std::set<SGPropertyNode*> changedNodes;
std::set<PropertyId> 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()
{
SGTimeStamp st;
st.stamp();
cJSON* result = cJSON_CreateObject();
int newSize = newNodes.size();
int changedSize = changedNodes.size();
int removedSize = removedNodes.size();
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, "position", cJSON_CreateNumber(prop->getPosition()));
cJSON_AddItemToObject(newPropData, "id", cJSON_CreateNumber(idForProperty(prop)));
if (prop->getType() != simgear::props::NONE) {
cJSON_AddItemToObject(newPropData, "value", JSON::valueToJson(prop));
}
cJSON_AddItemToArray(newNodesArray, newPropData);
}
newNodes.clear();
cJSON_AddItemToObject(result, "created", newNodesArray);
}
if (!removedNodes.empty()) {
cJSON * deletedNodesArray = cJSON_CreateArray();
for (auto propId : removedNodes) {
cJSON_AddItemToArray(deletedNodesArray, cJSON_CreateNumber(propId));
}
cJSON_AddItemToObject(result, "removed", deletedNodesArray);
removedNodes.clear();
}
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);
}
SG_LOG(SG_NETWORK, SG_INFO, "making JSON data took:" << st.elapsedMSec() << " for " << newSize << "/" << changedSize << "/" << removedSize);
recentlyRemoved.clear();
return result;
}
bool haveChangesToSend() const
{
return !newNodes.empty() || !changedNodes.empty() || !removedNodes.empty();
}
private:
PropertyId nextPropertyId = 1;
std::unordered_map<SGPropertyNode*, PropertyId> idHash;
std::vector<PropertyValue> previousValues;
/// track recently removed nodes in case they are re-created imemdiately
/// after with the same type, since we can make this much more efficient
/// when sending over the wire.
std::vector<RecentlyRemovedNode> recentlyRemoved;
};
#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<size_t>(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());
_listener->registerSubtree(_subtreeRoot);
_lastSendTime = SGTimeStamp::now();
}
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