mirror of
https://github.com/lubosz/overte.git
synced 2025-08-07 16:41:02 +02:00
Merge pull request #9518 from howard-stearns/entity-filter-resource
Entity filters by URL
This commit is contained in:
commit
d1a137cd18
5 changed files with 117 additions and 26 deletions
|
@ -9,9 +9,12 @@
|
||||||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||||
//
|
//
|
||||||
|
|
||||||
|
#include <QtCore/QEventLoop>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
#include <EntityTree.h>
|
#include <EntityTree.h>
|
||||||
#include <SimpleEntitySimulation.h>
|
#include <SimpleEntitySimulation.h>
|
||||||
|
#include <ResourceCache.h>
|
||||||
|
#include <ScriptCache.h>
|
||||||
|
|
||||||
#include "EntityServer.h"
|
#include "EntityServer.h"
|
||||||
#include "EntityServerConsts.h"
|
#include "EntityServerConsts.h"
|
||||||
|
@ -26,6 +29,10 @@ EntityServer::EntityServer(ReceivedMessage& message) :
|
||||||
OctreeServer(message),
|
OctreeServer(message),
|
||||||
_entitySimulation(NULL)
|
_entitySimulation(NULL)
|
||||||
{
|
{
|
||||||
|
ResourceManager::init();
|
||||||
|
DependencyManager::set<ResourceCacheSharedItems>();
|
||||||
|
DependencyManager::set<ScriptCache>();
|
||||||
|
|
||||||
auto& packetReceiver = DependencyManager::get<NodeList>()->getPacketReceiver();
|
auto& packetReceiver = DependencyManager::get<NodeList>()->getPacketReceiver();
|
||||||
packetReceiver.registerListenerForTypes({ PacketType::EntityAdd, PacketType::EntityEdit, PacketType::EntityErase },
|
packetReceiver.registerListenerForTypes({ PacketType::EntityAdd, PacketType::EntityEdit, PacketType::EntityErase },
|
||||||
this, "handleEntityPacket");
|
this, "handleEntityPacket");
|
||||||
|
@ -286,11 +293,96 @@ void EntityServer::readAdditionalConfiguration(const QJsonObject& settingsSectio
|
||||||
tree->setEntityScriptSourceWhitelist("");
|
tree->setEntityScriptSourceWhitelist("");
|
||||||
}
|
}
|
||||||
|
|
||||||
QString entityEditFilter;
|
if (readOptionString("entityEditFilter", settingsSectionObject, _entityEditFilter) && !_entityEditFilter.isEmpty()) {
|
||||||
if (readOptionString("entityEditFilter", settingsSectionObject, entityEditFilter)) {
|
// Fetch script from file synchronously. We don't want the server processing edits while a restarting entity server is fetching from a DOS'd source.
|
||||||
tree->setEntityEditFilter(entityEditFilter);
|
QUrl scriptURL(_entityEditFilter);
|
||||||
|
|
||||||
|
// The following should be abstracted out for use in Agent.cpp (and maybe later AvatarMixer.cpp)
|
||||||
|
if (scriptURL.scheme().isEmpty() || (scriptURL.scheme() == URL_SCHEME_FILE)) {
|
||||||
|
qWarning() << "Cannot load script from local filesystem, because assignment may be on a different computer.";
|
||||||
|
scriptRequestFinished();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto scriptRequest = ResourceManager::createResourceRequest(this, scriptURL);
|
||||||
|
if (!scriptRequest) {
|
||||||
|
qWarning() << "Could not create ResourceRequest for Agent script at" << scriptURL.toString();
|
||||||
|
scriptRequestFinished();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Agent.cpp sets up a timeout here, but that is unnecessary, as ResourceRequest has its own.
|
||||||
|
connect(scriptRequest, &ResourceRequest::finished, this, &EntityServer::scriptRequestFinished);
|
||||||
|
// FIXME: handle atp rquests setup here. See Agent::requestScript()
|
||||||
|
qInfo() << "Requesting script at URL" << qPrintable(scriptRequest->getUrl().toString());
|
||||||
|
scriptRequest->send();
|
||||||
|
_scriptRequestLoop.exec(); // Block here, but allow the request to be processed and its signals to be handled.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copied from ScriptEngine.cpp. We should make this a class method for reuse.
|
||||||
|
// Note: I've deliberately stopped short of using ScriptEngine instead of QScriptEngine, as that is out of project scope at this point.
|
||||||
|
static bool hasCorrectSyntax(const QScriptProgram& program) {
|
||||||
|
const auto syntaxCheck = QScriptEngine::checkSyntax(program.sourceCode());
|
||||||
|
if (syntaxCheck.state() != QScriptSyntaxCheckResult::Valid) {
|
||||||
|
const auto error = syntaxCheck.errorMessage();
|
||||||
|
const auto line = QString::number(syntaxCheck.errorLineNumber());
|
||||||
|
const auto column = QString::number(syntaxCheck.errorColumnNumber());
|
||||||
|
const auto message = QString("[SyntaxError] %1 in %2:%3(%4)").arg(error, program.fileName(), line, column);
|
||||||
|
qCritical() << qPrintable(message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
static bool hadUncaughtExceptions(QScriptEngine& engine, const QString& fileName) {
|
||||||
|
if (engine.hasUncaughtException()) {
|
||||||
|
const auto backtrace = engine.uncaughtExceptionBacktrace();
|
||||||
|
const auto exception = engine.uncaughtException().toString();
|
||||||
|
const auto line = QString::number(engine.uncaughtExceptionLineNumber());
|
||||||
|
engine.clearExceptions();
|
||||||
|
|
||||||
|
static const QString SCRIPT_EXCEPTION_FORMAT = "[UncaughtException] %1 in %2:%3";
|
||||||
|
auto message = QString(SCRIPT_EXCEPTION_FORMAT).arg(exception, fileName, line);
|
||||||
|
if (!backtrace.empty()) {
|
||||||
|
static const auto lineSeparator = "\n ";
|
||||||
|
message += QString("\n[Backtrace]%1%2").arg(lineSeparator, backtrace.join(lineSeparator));
|
||||||
|
}
|
||||||
|
qCritical() << qPrintable(message);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
void EntityServer::scriptRequestFinished() {
|
||||||
|
auto scriptRequest = qobject_cast<ResourceRequest*>(sender());
|
||||||
|
const QString urlString = scriptRequest->getUrl().toString();
|
||||||
|
if (scriptRequest && scriptRequest->getResult() == ResourceRequest::Success) {
|
||||||
|
auto scriptContents = scriptRequest->getData();
|
||||||
|
qInfo() << "Downloaded script:" << scriptContents;
|
||||||
|
QScriptProgram program(scriptContents, urlString);
|
||||||
|
if (hasCorrectSyntax(program)) {
|
||||||
|
_entityEditFilterEngine.evaluate(scriptContents);
|
||||||
|
if (!hadUncaughtExceptions(_entityEditFilterEngine, urlString)) {
|
||||||
|
std::static_pointer_cast<EntityTree>(_tree)->initEntityEditFilterEngine(&_entityEditFilterEngine, [this]() {
|
||||||
|
return hadUncaughtExceptions(_entityEditFilterEngine, _entityEditFilter);
|
||||||
|
});
|
||||||
|
scriptRequest->deleteLater();
|
||||||
|
if (_scriptRequestLoop.isRunning()) {
|
||||||
|
_scriptRequestLoop.quit();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (scriptRequest) {
|
||||||
|
qCritical() << "Failed to download script at" << urlString;
|
||||||
|
// See HTTPResourceRequest::onRequestFinished for interpretation of codes. For example, a 404 is code 6 and 403 is 3. A timeout is 2. Go figure.
|
||||||
|
qCritical() << "ResourceRequest error was" << scriptRequest->getResult();
|
||||||
|
} else {
|
||||||
|
qCritical() << "Failed to create script request.";
|
||||||
|
}
|
||||||
|
// Hard stop of the assignment client on failure. We don't want anyone to think they have a filter in place when they don't.
|
||||||
|
// Alas, only indications will be the above logging with assignment client restarting repeatedly, and clients will not see any entities.
|
||||||
|
stop();
|
||||||
|
if (_scriptRequestLoop.isRunning()) {
|
||||||
|
_scriptRequestLoop.quit();
|
||||||
}
|
}
|
||||||
tree->initEntityEditFilterEngine(); // whether supplied or not.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void EntityServer::nodeAdded(SharedNodePointer node) {
|
void EntityServer::nodeAdded(SharedNodePointer node) {
|
||||||
|
|
|
@ -69,6 +69,7 @@ protected:
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void handleEntityPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode);
|
void handleEntityPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode);
|
||||||
|
void scriptRequestFinished();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
SimpleEntitySimulationPointer _entitySimulation;
|
SimpleEntitySimulationPointer _entitySimulation;
|
||||||
|
@ -76,6 +77,10 @@ private:
|
||||||
|
|
||||||
QReadWriteLock _viewerSendingStatsLock;
|
QReadWriteLock _viewerSendingStatsLock;
|
||||||
QMap<QUuid, QMap<QUuid, ViewerSendingStats>> _viewerSendingStats;
|
QMap<QUuid, QMap<QUuid, ViewerSendingStats>> _viewerSendingStats;
|
||||||
|
|
||||||
|
QString _entityEditFilter{};
|
||||||
|
QScriptEngine _entityEditFilterEngine{};
|
||||||
|
QEventLoop _scriptRequestLoop{};
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // hifi_EntityServer_h
|
#endif // hifi_EntityServer_h
|
||||||
|
|
|
@ -1294,7 +1294,7 @@
|
||||||
"name": "entityEditFilter",
|
"name": "entityEditFilter",
|
||||||
"label": "Filter Entity Edits",
|
"label": "Filter Entity Edits",
|
||||||
"help": "Check all entity edits against this filter function.",
|
"help": "Check all entity edits against this filter function.",
|
||||||
"placeholder": "function filter(properties) { return properties; }",
|
"placeholder": "url whose content is like: function filter(properties) { return properties; }",
|
||||||
"default": "",
|
"default": "",
|
||||||
"advanced": true
|
"advanced": true
|
||||||
},
|
},
|
||||||
|
|
|
@ -922,15 +922,16 @@ void EntityTree::fixupTerseEditLogging(EntityItemProperties& properties, QList<Q
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void EntityTree::initEntityEditFilterEngine() {
|
void EntityTree::initEntityEditFilterEngine(QScriptEngine* engine, std::function<bool()> entityEditFilterHadUncaughtExceptions) {
|
||||||
_entityEditFilterEngine.evaluate(_entityEditFilter);
|
_entityEditFilterEngine = engine;
|
||||||
auto global = _entityEditFilterEngine.globalObject();
|
_entityEditFilterHadUncaughtExceptions = entityEditFilterHadUncaughtExceptions;
|
||||||
|
auto global = _entityEditFilterEngine->globalObject();
|
||||||
_entityEditFilterFunction = global.property("filter");
|
_entityEditFilterFunction = global.property("filter");
|
||||||
_hasEntityEditFilter = _entityEditFilterFunction.isFunction();
|
_hasEntityEditFilter = _entityEditFilterFunction.isFunction();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool EntityTree::filterProperties(EntityItemProperties& propertiesIn, EntityItemProperties& propertiesOut, bool& wasChanged) {
|
bool EntityTree::filterProperties(EntityItemProperties& propertiesIn, EntityItemProperties& propertiesOut, bool& wasChanged) {
|
||||||
if (!_hasEntityEditFilter) {
|
if (!_hasEntityEditFilter || !_entityEditFilterEngine) {
|
||||||
propertiesOut = propertiesIn;
|
propertiesOut = propertiesIn;
|
||||||
wasChanged = false; // not changed
|
wasChanged = false; // not changed
|
||||||
return true; // allowed
|
return true; // allowed
|
||||||
|
@ -938,7 +939,7 @@ bool EntityTree::filterProperties(EntityItemProperties& propertiesIn, EntityItem
|
||||||
auto oldProperties = propertiesIn.getDesiredProperties();
|
auto oldProperties = propertiesIn.getDesiredProperties();
|
||||||
auto specifiedProperties = propertiesIn.getChangedProperties();
|
auto specifiedProperties = propertiesIn.getChangedProperties();
|
||||||
propertiesIn.setDesiredProperties(specifiedProperties);
|
propertiesIn.setDesiredProperties(specifiedProperties);
|
||||||
QScriptValue inputValues = propertiesIn.copyToScriptValue(&_entityEditFilterEngine, false, true, true);
|
QScriptValue inputValues = propertiesIn.copyToScriptValue(_entityEditFilterEngine, false, true, true);
|
||||||
propertiesIn.setDesiredProperties(oldProperties);
|
propertiesIn.setDesiredProperties(oldProperties);
|
||||||
|
|
||||||
auto in = QJsonValue::fromVariant(inputValues.toVariant()); // grab json copy now, because the inputValues might be side effected by the filter.
|
auto in = QJsonValue::fromVariant(inputValues.toVariant()); // grab json copy now, because the inputValues might be side effected by the filter.
|
||||||
|
@ -946,22 +947,16 @@ bool EntityTree::filterProperties(EntityItemProperties& propertiesIn, EntityItem
|
||||||
args << inputValues;
|
args << inputValues;
|
||||||
|
|
||||||
QScriptValue result = _entityEditFilterFunction.call(_nullObjectForFilter, args);
|
QScriptValue result = _entityEditFilterFunction.call(_nullObjectForFilter, args);
|
||||||
|
if (_entityEditFilterHadUncaughtExceptions()) {
|
||||||
|
result = QScriptValue();
|
||||||
|
}
|
||||||
|
|
||||||
propertiesOut.copyFromScriptValue(result, false);
|
|
||||||
bool accepted = result.isObject(); // filters should return null or false to completely reject edit or add
|
bool accepted = result.isObject(); // filters should return null or false to completely reject edit or add
|
||||||
if (accepted) {
|
if (accepted) {
|
||||||
|
propertiesOut.copyFromScriptValue(result, false);
|
||||||
// Javascript objects are == only if they are the same object. To compare arbitrary values, we need to use JSON.
|
// 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());
|
auto out = QJsonValue::fromVariant(result.toVariant());
|
||||||
wasChanged = in != out;
|
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;
|
return accepted;
|
||||||
|
|
|
@ -68,7 +68,6 @@ public:
|
||||||
|
|
||||||
void setEntityMaxTmpLifetime(float maxTmpEntityLifetime) { _maxTmpEntityLifetime = maxTmpEntityLifetime; }
|
void setEntityMaxTmpLifetime(float maxTmpEntityLifetime) { _maxTmpEntityLifetime = maxTmpEntityLifetime; }
|
||||||
void setEntityScriptSourceWhitelist(const QString& entityScriptSourceWhitelist);
|
void setEntityScriptSourceWhitelist(const QString& entityScriptSourceWhitelist);
|
||||||
void setEntityEditFilter(const QString& entityEditFilter) { _entityEditFilter = entityEditFilter; }
|
|
||||||
|
|
||||||
/// Implements our type specific root element factory
|
/// Implements our type specific root element factory
|
||||||
virtual OctreeElementPointer createNewElement(unsigned char* octalCode = NULL) override;
|
virtual OctreeElementPointer createNewElement(unsigned char* octalCode = NULL) override;
|
||||||
|
@ -267,7 +266,7 @@ public:
|
||||||
|
|
||||||
void notifyNewCollisionSoundURL(const QString& newCollisionSoundURL, const EntityItemID& entityID);
|
void notifyNewCollisionSoundURL(const QString& newCollisionSoundURL, const EntityItemID& entityID);
|
||||||
|
|
||||||
void initEntityEditFilterEngine();
|
void initEntityEditFilterEngine(QScriptEngine* engine, std::function<bool()> entityEditFilterHadUncaughtExceptions);
|
||||||
|
|
||||||
static const float DEFAULT_MAX_TMP_ENTITY_LIFETIME;
|
static const float DEFAULT_MAX_TMP_ENTITY_LIFETIME;
|
||||||
|
|
||||||
|
@ -358,11 +357,11 @@ protected:
|
||||||
float _maxTmpEntityLifetime { DEFAULT_MAX_TMP_ENTITY_LIFETIME };
|
float _maxTmpEntityLifetime { DEFAULT_MAX_TMP_ENTITY_LIFETIME };
|
||||||
|
|
||||||
bool filterProperties(EntityItemProperties& propertiesIn, EntityItemProperties& propertiesOut, bool& wasChanged);
|
bool filterProperties(EntityItemProperties& propertiesIn, EntityItemProperties& propertiesOut, bool& wasChanged);
|
||||||
QString _entityEditFilter;
|
|
||||||
bool _hasEntityEditFilter{ false };
|
bool _hasEntityEditFilter{ false };
|
||||||
QScriptEngine _entityEditFilterEngine;
|
QScriptEngine* _entityEditFilterEngine{};
|
||||||
QScriptValue _entityEditFilterFunction;
|
QScriptValue _entityEditFilterFunction{};
|
||||||
QScriptValue _nullObjectForFilter;
|
QScriptValue _nullObjectForFilter{};
|
||||||
|
std::function<bool()> _entityEditFilterHadUncaughtExceptions;
|
||||||
|
|
||||||
QStringList _entityScriptSourceWhitelist;
|
QStringList _entityScriptSourceWhitelist;
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue