Merge pull request #9477 from howard-stearns/minimum-edit-entity-filter

Minimum edit entity filter
This commit is contained in:
Howard Stearns 2017-01-23 18:04:52 -08:00 committed by GitHub
commit f3956b3fb0
10 changed files with 136 additions and 21 deletions

View file

@ -285,6 +285,12 @@ void EntityServer::readAdditionalConfiguration(const QJsonObject& settingsSectio
} else {
tree->setEntityScriptSourceWhitelist("");
}
QString entityEditFilter;
if (readOptionString("entityEditFilter", settingsSectionObject, entityEditFilter)) {
tree->setEntityEditFilter(entityEditFilter);
}
tree->initEntityEditFilterEngine(); // whether supplied or not.
}
void EntityServer::nodeAdded(SharedNodePointer node) {

View file

@ -660,6 +660,7 @@ bool OctreeServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url
quint64 averageUpdateTime = _tree->getAverageUpdateTime();
quint64 averageCreateTime = _tree->getAverageCreateTime();
quint64 averageLoggingTime = _tree->getAverageLoggingTime();
quint64 averageFilterTime = _tree->getAverageFilterTime();
int FLOAT_PRECISION = 3;
@ -699,6 +700,8 @@ bool OctreeServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url
.arg(locale.toString((uint)averageCreateTime).rightJustified(COLUMN_WIDTH, ' '));
statsString += QString(" Average Logging Time: %1 usecs\r\n")
.arg(locale.toString((uint)averageLoggingTime).rightJustified(COLUMN_WIDTH, ' '));
statsString += QString(" Average Filter Time: %1 usecs\r\n")
.arg(locale.toString((uint)averageFilterTime).rightJustified(COLUMN_WIDTH, ' '));
int senderNumber = 0;

View file

@ -1290,6 +1290,14 @@
"default": "",
"advanced": true
},
{
"name": "entityEditFilter",
"label": "Filter Entity Edits",
"help": "Check all entity edits against this filter function.",
"placeholder": "function filter(properties) { return properties; }",
"default": "",
"advanced": true
},
{
"name": "persistFilePath",
"label": "Entities File Path",

View file

@ -349,7 +349,6 @@ int EntityItem::expectedBytes() {
return MINIMUM_HEADER_BYTES;
}
// clients use this method to unpack FULL updates from entity-server
int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLeftToRead, ReadBitstreamToTreeParams& args) {
if (args.bitstreamVersion < VERSION_ENTITIES_SUPPORT_SPLIT_MTU) {
@ -667,6 +666,9 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef
// entity-server is trying to clear our ownership (probably at our own request)
// but we actually want to own it, therefore we ignore this clear event
// and pretend that we own it (we assume we'll recover it soon)
// However, for now, when the server uses a newer time than what we sent, listen to what we're told.
if (overwriteLocalData) weOwnSimulation = false;
} else if (_simulationOwner.set(newSimOwner)) {
_dirtyFlags |= Simulation::DIRTY_SIMULATOR_ID;
somethingChanged = true;

View file

@ -346,11 +346,15 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const {
return changedProperties;
}
QScriptValue EntityItemProperties::copyToScriptValue(QScriptEngine* engine, bool skipDefaults) const {
QScriptValue EntityItemProperties::copyToScriptValue(QScriptEngine* engine, bool skipDefaults, bool allowUnknownCreateTime, bool strictSemantics) const {
// If strictSemantics is true and skipDefaults is false, then all and only those properties are copied for which the property flag
// is included in _desiredProperties, or is one of the specially enumerated ALWAYS properties below.
// (There may be exceptions, but if so, they are bugs.)
// In all other cases, you are welcome to inspect the code and try to figure out what was intended. I wish you luck. -HRS 1/18/17
QScriptValue properties = engine->newObject();
EntityItemProperties defaultEntityProperties;
if (_created == UNKNOWN_CREATED_TIME) {
if (_created == UNKNOWN_CREATED_TIME && !allowUnknownCreateTime) {
// No entity properties can have been set so return without setting any default, zero property values.
return properties;
}
@ -364,7 +368,7 @@ QScriptValue EntityItemProperties::copyToScriptValue(QScriptEngine* engine, bool
created.setTimeSpec(Qt::OffsetFromUTC);
COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER_ALWAYS(created, created.toString(Qt::ISODate));
if (!skipDefaults || _lifetime != defaultEntityProperties._lifetime) {
if ((!skipDefaults || _lifetime != defaultEntityProperties._lifetime) && !strictSemantics) {
COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER_NO_SKIP(age, getAge()); // gettable, but not settable
COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER_NO_SKIP(ageAsText, formatSecondsElapsed(getAge())); // gettable, but not settable
}
@ -539,7 +543,7 @@ QScriptValue EntityItemProperties::copyToScriptValue(QScriptEngine* engine, bool
}
// Sitting properties support
if (!skipDefaults) {
if (!skipDefaults && !strictSemantics) {
QScriptValue sittingPoints = engine->newObject();
for (int i = 0; i < _sittingPoints.size(); ++i) {
QScriptValue sittingPoint = engine->newObject();
@ -552,7 +556,7 @@ QScriptValue EntityItemProperties::copyToScriptValue(QScriptEngine* engine, bool
COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER_ALWAYS(sittingPoints, sittingPoints); // gettable, but not settable
}
if (!skipDefaults) {
if (!skipDefaults && !strictSemantics) {
AABox aaBox = getAABox();
QScriptValue boundingBox = engine->newObject();
QScriptValue bottomRightNear = vec3toScriptValue(engine, aaBox.getCorner());
@ -567,7 +571,7 @@ QScriptValue EntityItemProperties::copyToScriptValue(QScriptEngine* engine, bool
}
QString textureNamesStr = QJsonDocument::fromVariant(_textureNames).toJson();
if (!skipDefaults) {
if (!skipDefaults && !strictSemantics) {
COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER_NO_SKIP(originalTextures, textureNamesStr); // gettable, but not settable
}
@ -584,7 +588,7 @@ QScriptValue EntityItemProperties::copyToScriptValue(QScriptEngine* engine, bool
COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_OWNING_AVATAR_ID, owningAvatarID);
// Rendering info
if (!skipDefaults) {
if (!skipDefaults && !strictSemantics) {
QScriptValue renderInfo = engine->newObject();
// currently only supported by models

View file

@ -73,7 +73,7 @@ public:
EntityTypes::EntityType getType() const { return _type; }
void setType(EntityTypes::EntityType type) { _type = type; }
virtual QScriptValue copyToScriptValue(QScriptEngine* engine, bool skipDefaults) const;
virtual QScriptValue copyToScriptValue(QScriptEngine* engine, bool skipDefaults, bool allowUnknownCreateTime = false, bool strictSemantics = false) const;
virtual void copyFromScriptValue(const QScriptValue& object, bool honorReadOnly);
static QScriptValue entityPropertyFlagsToScriptValue(QScriptEngine* engine, const EntityPropertyFlags& flags);
@ -93,6 +93,8 @@ public:
void debugDump() const;
void setLastEdited(quint64 usecTime);
EntityPropertyFlags getDesiredProperties() { return _desiredProperties; }
void setDesiredProperties(EntityPropertyFlags properties) { _desiredProperties = properties; }
// Note: DEFINE_PROPERTY(PROP_FOO, Foo, foo, type, value) creates the following methods and variables:
// type getFoo() const;
@ -460,10 +462,6 @@ inline QDebug operator<<(QDebug debug, const EntityItemProperties& properties) {
DEBUG_PROPERTY_IF_CHANGED(debug, properties, LastEditedBy, lastEditedBy, "");
properties.getAnimation().debugDump();
properties.getSkybox().debugDump();
properties.getStage().debugDump();
debug << " last edited:" << properties.getLastEdited() << "\n";
debug << " edited ago:" << properties.getEditedAgo() << "\n";
debug << "]";

View file

@ -918,6 +918,61 @@ void EntityTree::fixupTerseEditLogging(EntityItemProperties& properties, QList<Q
}
}
void EntityTree::initEntityEditFilterEngine() {
_entityEditFilterEngine.evaluate(_entityEditFilter);
auto global = _entityEditFilterEngine.globalObject();
_entityEditFilterFunction = global.property("filter");
_hasEntityEditFilter = _entityEditFilterFunction.isFunction();
}
bool EntityTree::filterProperties(EntityItemProperties& propertiesIn, EntityItemProperties& propertiesOut, bool& wasChanged) {
if (!_hasEntityEditFilter) {
propertiesOut = propertiesIn;
wasChanged = false; // not changed
return true; // allowed
}
auto oldProperties = propertiesIn.getDesiredProperties();
auto specifiedProperties = propertiesIn.getChangedProperties();
propertiesIn.setDesiredProperties(specifiedProperties);
QScriptValue inputValues = propertiesIn.copyToScriptValue(&_entityEditFilterEngine, false, true, true);
propertiesIn.setDesiredProperties(oldProperties);
auto in = QJsonValue::fromVariant(inputValues.toVariant()); // grab json copy now, because the inputValues might be side effected by the filter.
QScriptValueList args;
args << inputValues;
QScriptValue result = _entityEditFilterFunction.call(_nullObjectForFilter, args);
propertiesOut.copyFromScriptValue(result, false);
bool accepted = result.isObject(); // filters should return null or false to completely reject edit or add
if (accepted) {
// Javascript objects are == only if they are the same object. To compare arbitrary values, we need to use JSON.
auto out = QJsonValue::fromVariant(result.toVariant());
wasChanged = in != out;
if (wasChanged) {
// Logging will be removed eventually, but for now, the behavior is so fragile that it's worth logging.
qCDebug(entities) << "filter accepted. changed: true";
qCDebug(entities) << " in:" << in;
qCDebug(entities) << " out:" << out;
}
} else {
qCDebug(entities) << "filter rejected. in:" << in;
}
return accepted;
}
void EntityTree::bumpTimestamp(EntityItemProperties& properties) { //fixme put class/header
const quint64 LAST_EDITED_SERVERSIDE_BUMP = 1; // usec
// also bump up the lastEdited time of the properties so that the interface that created this edit
// will accept our adjustment to lifetime back into its own entity-tree.
if (properties.getLastEdited() == UNKNOWN_CREATED_TIME) {
properties.setLastEdited(usecTimestampNow());
}
properties.setLastEdited(properties.getLastEdited() + LAST_EDITED_SERVERSIDE_BUMP);
}
int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned char* editData, int maxLength,
const SharedNodePointer& senderNode) {
@ -941,9 +996,9 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c
quint64 startLookup = 0, endLookup = 0;
quint64 startUpdate = 0, endUpdate = 0;
quint64 startCreate = 0, endCreate = 0;
quint64 startFilter = 0, endFilter = 0;
quint64 startLogging = 0, endLogging = 0;
const quint64 LAST_EDITED_SERVERSIDE_BUMP = 1; // usec
bool suppressDisallowedScript = false;
_totalEditMessages++;
@ -988,18 +1043,27 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c
if (properties.getLifetime() == ENTITY_ITEM_IMMORTAL_LIFETIME ||
properties.getLifetime() > _maxTmpEntityLifetime) {
properties.setLifetime(_maxTmpEntityLifetime);
// also bump up the lastEdited time of the properties so that the interface that created this edit
// will accept our adjustment to lifetime back into its own entity-tree.
if (properties.getLastEdited() == UNKNOWN_CREATED_TIME) {
properties.setLastEdited(usecTimestampNow());
}
properties.setLastEdited(properties.getLastEdited() + LAST_EDITED_SERVERSIDE_BUMP);
bumpTimestamp(properties);
}
}
// If we got a valid edit packet, then it could be a new entity or it could be an update to
// an existing entity... handle appropriately
if (validEditPacket) {
startFilter = usecTimestampNow();
bool wasChanged = false;
bool allowed = filterProperties(properties, properties, wasChanged);
if (!allowed) {
properties = EntityItemProperties();
}
if (!allowed || wasChanged) {
bumpTimestamp(properties);
// For now, free ownership on any modification.
properties.clearSimulationOwner();
}
endFilter = usecTimestampNow();
// search for the entity by EntityItemID
startLookup = usecTimestampNow();
EntityItemPointer existingEntity = findEntityByEntityItemID(entityItemID);
@ -1007,7 +1071,7 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c
if (existingEntity && message.getType() == PacketType::EntityEdit) {
if (suppressDisallowedScript) {
properties.setLastEdited(properties.getLastEdited() + LAST_EDITED_SERVERSIDE_BUMP);
bumpTimestamp(properties);
properties.setScript(existingEntity->getScript());
}
@ -1077,6 +1141,7 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c
_totalUpdateTime += endUpdate - startUpdate;
_totalCreateTime += endCreate - startCreate;
_totalLoggingTime += endLogging - startLogging;
_totalFilterTime += endFilter - startFilter;
break;
}

View file

@ -65,6 +65,7 @@ public:
void setEntityMaxTmpLifetime(float maxTmpEntityLifetime) { _maxTmpEntityLifetime = maxTmpEntityLifetime; }
void setEntityScriptSourceWhitelist(const QString& entityScriptSourceWhitelist);
void setEntityEditFilter(const QString& entityEditFilter) { _entityEditFilter = entityEditFilter; }
/// Implements our type specific root element factory
virtual OctreeElementPointer createNewElement(unsigned char* octalCode = NULL) override;
@ -235,6 +236,7 @@ public:
virtual quint64 getAverageUpdateTime() const override { return _totalUpdates == 0 ? 0 : _totalUpdateTime / _totalUpdates; }
virtual quint64 getAverageCreateTime() const override { return _totalCreates == 0 ? 0 : _totalCreateTime / _totalCreates; }
virtual quint64 getAverageLoggingTime() const override { return _totalEditMessages == 0 ? 0 : _totalLoggingTime / _totalEditMessages; }
virtual quint64 getAverageFilterTime() const override { return _totalEditMessages == 0 ? 0 : _totalFilterTime / _totalEditMessages; }
void trackIncomingEntityLastEdited(quint64 lastEditedTime, int bytesRead);
quint64 getAverageEditDeltas() const
@ -261,6 +263,8 @@ public:
void notifyNewCollisionSoundURL(const QString& newCollisionSoundURL, const EntityItemID& entityID);
void initEntityEditFilterEngine();
static const float DEFAULT_MAX_TMP_ENTITY_LIFETIME;
public slots:
@ -285,6 +289,7 @@ protected:
static bool findInBoxOperation(OctreeElementPointer element, void* extraData);
static bool findInFrustumOperation(OctreeElementPointer element, void* extraData);
static bool sendEntitiesOperation(OctreeElementPointer element, void* extraData);
static void bumpTimestamp(EntityItemProperties& properties);
void notifyNewlyCreatedEntity(const EntityItem& newEntity, const SharedNodePointer& senderNode);
@ -327,6 +332,7 @@ protected:
quint64 _totalUpdateTime = 0;
quint64 _totalCreateTime = 0;
quint64 _totalLoggingTime = 0;
quint64 _totalFilterTime = 0;
// these performance statistics are only used in the client
void resetClientEditStats();
@ -346,6 +352,13 @@ protected:
float _maxTmpEntityLifetime { DEFAULT_MAX_TMP_ENTITY_LIFETIME };
bool filterProperties(EntityItemProperties& propertiesIn, EntityItemProperties& propertiesOut, bool& wasChanged);
QString _entityEditFilter;
bool _hasEntityEditFilter{ false };
QScriptEngine _entityEditFilterEngine;
QScriptValue _entityEditFilterFunction;
QScriptValue _nullObjectForFilter;
QStringList _entityScriptSourceWhitelist;
};

View file

@ -349,6 +349,7 @@ public:
virtual quint64 getAverageUpdateTime() const { return 0; }
virtual quint64 getAverageCreateTime() const { return 0; }
virtual quint64 getAverageLoggingTime() const { return 0; }
virtual quint64 getAverageFilterTime() const { return 0; }
signals:
void importSize(float x, float y, float z);

View file

@ -0,0 +1,15 @@
function filter(p) {
/* block comments are ok, but not double-slash end-of-line-comments */
/* Simple example: if someone specifies name, add an 'x' to it. Note that print is ok to use. */
if (p.name) {p.name += 'x'; print('fixme name', p. name);}
/* This example clamps y. A better filter would probably zero y component of velocity and acceleration. */
if (p.position) {p.position.y = Math.min(1, p.position.y); print('fixme p.y', p.position.y);}
/* Can also reject altogether */
if (p.userData) { return false; }
/* If we make it this far, return the (possibly modified) properties. */
return p;
}