mirror of
https://github.com/lubosz/overte.git
synced 2025-04-24 09:23:17 +02:00
Merge pull request #9686 from humbletim/21114-part3
CR 21114 -- Implement modules/require support into hifi
This commit is contained in:
commit
308c134119
29 changed files with 1538 additions and 138 deletions
|
@ -146,6 +146,7 @@ void EntityTreeRenderer::clear() {
|
|||
|
||||
void EntityTreeRenderer::reloadEntityScripts() {
|
||||
_entitiesScriptEngine->unloadAllEntityScripts();
|
||||
_entitiesScriptEngine->resetModuleCache();
|
||||
foreach(auto entity, _entitiesInScene) {
|
||||
if (!entity->getScript().isEmpty()) {
|
||||
_entitiesScriptEngine->loadEntityScript(entity->getEntityItemID(), entity->getScript(), true);
|
||||
|
|
|
@ -15,11 +15,13 @@
|
|||
#define hifi_EntitiesScriptEngineProvider_h
|
||||
|
||||
#include <QtCore/QString>
|
||||
#include <QFuture>
|
||||
#include "EntityItemID.h"
|
||||
|
||||
class EntitiesScriptEngineProvider {
|
||||
public:
|
||||
virtual void callEntityScriptMethod(const EntityItemID& entityID, const QString& methodName, const QStringList& params = QStringList()) = 0;
|
||||
virtual QFuture<QVariant> getLocalEntityScriptDetails(const EntityItemID& entityID) = 0;
|
||||
};
|
||||
|
||||
#endif // hifi_EntitiesScriptEngineProvider_h
|
||||
#endif // hifi_EntitiesScriptEngineProvider_h
|
||||
|
|
|
@ -10,6 +10,9 @@
|
|||
//
|
||||
#include "EntityScriptingInterface.h"
|
||||
|
||||
#include <QFutureWatcher>
|
||||
#include <QtConcurrent/QtConcurrentRun>
|
||||
|
||||
#include "EntityItemID.h"
|
||||
#include <VariantMapToScriptValue.h>
|
||||
#include <SharedUtil.h>
|
||||
|
@ -680,6 +683,118 @@ bool EntityScriptingInterface::reloadServerScripts(QUuid entityID) {
|
|||
return client->reloadServerScript(entityID);
|
||||
}
|
||||
|
||||
bool EntityPropertyMetadataRequest::script(EntityItemID entityID, QScriptValue handler) {
|
||||
using LocalScriptStatusRequest = QFutureWatcher<QVariant>;
|
||||
|
||||
LocalScriptStatusRequest* request = new LocalScriptStatusRequest;
|
||||
QObject::connect(request, &LocalScriptStatusRequest::finished, _engine, [=]() mutable {
|
||||
auto details = request->result().toMap();
|
||||
QScriptValue err, result;
|
||||
if (details.contains("isError")) {
|
||||
if (!details.contains("message")) {
|
||||
details["message"] = details["errorInfo"];
|
||||
}
|
||||
err = _engine->makeError(_engine->toScriptValue(details));
|
||||
} else {
|
||||
details["success"] = true;
|
||||
result = _engine->toScriptValue(details);
|
||||
}
|
||||
callScopedHandlerObject(handler, err, result);
|
||||
request->deleteLater();
|
||||
});
|
||||
auto entityScriptingInterface = DependencyManager::get<EntityScriptingInterface>();
|
||||
entityScriptingInterface->withEntitiesScriptEngine([&](EntitiesScriptEngineProvider* entitiesScriptEngine) {
|
||||
if (entitiesScriptEngine) {
|
||||
request->setFuture(entitiesScriptEngine->getLocalEntityScriptDetails(entityID));
|
||||
}
|
||||
});
|
||||
if (!request->isStarted()) {
|
||||
request->deleteLater();
|
||||
callScopedHandlerObject(handler, _engine->makeError("Entities Scripting Provider unavailable", "InternalError"), QScriptValue());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool EntityPropertyMetadataRequest::serverScripts(EntityItemID entityID, QScriptValue handler) {
|
||||
auto client = DependencyManager::get<EntityScriptClient>();
|
||||
auto request = client->createScriptStatusRequest(entityID);
|
||||
QPointer<BaseScriptEngine> engine = _engine;
|
||||
QObject::connect(request, &GetScriptStatusRequest::finished, _engine, [=](GetScriptStatusRequest* request) mutable {
|
||||
auto engine = _engine;
|
||||
if (!engine) {
|
||||
qCDebug(entities) << __FUNCTION__ << " -- engine destroyed while inflight" << entityID;
|
||||
return;
|
||||
}
|
||||
QVariantMap details;
|
||||
details["success"] = request->getResponseReceived();
|
||||
details["isRunning"] = request->getIsRunning();
|
||||
details["status"] = EntityScriptStatus_::valueToKey(request->getStatus()).toLower();
|
||||
details["errorInfo"] = request->getErrorInfo();
|
||||
|
||||
QScriptValue err, result;
|
||||
if (!details["success"].toBool()) {
|
||||
if (!details.contains("message") && details.contains("errorInfo")) {
|
||||
details["message"] = details["errorInfo"];
|
||||
}
|
||||
if (details["message"].toString().isEmpty()) {
|
||||
details["message"] = "entity server script details not found";
|
||||
}
|
||||
err = engine->makeError(engine->toScriptValue(details));
|
||||
} else {
|
||||
result = engine->toScriptValue(details);
|
||||
}
|
||||
callScopedHandlerObject(handler, err, result);
|
||||
request->deleteLater();
|
||||
});
|
||||
request->start();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool EntityScriptingInterface::queryPropertyMetadata(QUuid entityID, QScriptValue property, QScriptValue scopeOrCallback, QScriptValue methodOrName) {
|
||||
auto name = property.toString();
|
||||
auto handler = makeScopedHandlerObject(scopeOrCallback, methodOrName);
|
||||
QPointer<BaseScriptEngine> engine = dynamic_cast<BaseScriptEngine*>(handler.engine());
|
||||
if (!engine) {
|
||||
qCDebug(entities) << "queryPropertyMetadata without detectable engine" << entityID << name;
|
||||
return false;
|
||||
}
|
||||
#ifdef DEBUG_ENGINE_STATE
|
||||
connect(engine, &QObject::destroyed, this, [=]() {
|
||||
qDebug() << "queryPropertyMetadata -- engine destroyed!" << (!engine ? "nullptr" : "engine");
|
||||
});
|
||||
#endif
|
||||
if (!handler.property("callback").isFunction()) {
|
||||
qDebug() << "!handler.callback.isFunction" << engine;
|
||||
engine->raiseException(engine->makeError("callback is not a function", "TypeError"));
|
||||
return false;
|
||||
}
|
||||
|
||||
// NOTE: this approach is a work-in-progress and for now just meant to work 100% correctly and provide
|
||||
// some initial structure for organizing metadata adapters around.
|
||||
|
||||
// The extra layer of indirection is *essential* because in real world conditions errors are often introduced
|
||||
// by accident and sometimes without exact memory of "what just changed."
|
||||
|
||||
// Here the scripter only needs to know an entityID and a property name -- which means all scripters can
|
||||
// level this method when stuck in dead-end scenarios or to learn more about "magic" Entity properties
|
||||
// like .script that work in terms of side-effects.
|
||||
|
||||
// This is an async callback pattern -- so if needed C++ can easily throttle or restrict queries later.
|
||||
|
||||
EntityPropertyMetadataRequest request(engine);
|
||||
|
||||
if (name == "script") {
|
||||
return request.script(entityID, handler);
|
||||
} else if (name == "serverScripts") {
|
||||
return request.serverScripts(entityID, handler);
|
||||
} else {
|
||||
engine->raiseException(engine->makeError("metadata for property " + name + " is not yet queryable"));
|
||||
engine->maybeEmitUncaughtException(__FUNCTION__);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool EntityScriptingInterface::getServerScriptStatus(QUuid entityID, QScriptValue callback) {
|
||||
auto client = DependencyManager::get<EntityScriptClient>();
|
||||
auto request = client->createScriptStatusRequest(entityID);
|
||||
|
|
|
@ -34,9 +34,24 @@
|
|||
#include "EntitiesScriptEngineProvider.h"
|
||||
#include "EntityItemProperties.h"
|
||||
|
||||
#include "BaseScriptEngine.h"
|
||||
|
||||
class EntityTree;
|
||||
class MeshProxy;
|
||||
|
||||
// helper factory to compose standardized, async metadata queries for "magic" Entity properties
|
||||
// like .script and .serverScripts. This is used for automated testing of core scripting features
|
||||
// as well as to provide early adopters a self-discoverable, consistent way to diagnose common
|
||||
// problems with their own Entity scripts.
|
||||
class EntityPropertyMetadataRequest {
|
||||
public:
|
||||
EntityPropertyMetadataRequest(BaseScriptEngine* engine) : _engine(engine) {};
|
||||
bool script(EntityItemID entityID, QScriptValue handler);
|
||||
bool serverScripts(EntityItemID entityID, QScriptValue handler);
|
||||
private:
|
||||
QPointer<BaseScriptEngine> _engine;
|
||||
};
|
||||
|
||||
class RayToEntityIntersectionResult {
|
||||
public:
|
||||
RayToEntityIntersectionResult();
|
||||
|
@ -68,6 +83,7 @@ class EntityScriptingInterface : public OctreeScriptingInterface, public Depende
|
|||
Q_PROPERTY(float costMultiplier READ getCostMultiplier WRITE setCostMultiplier)
|
||||
Q_PROPERTY(QUuid keyboardFocusEntity READ getKeyboardFocusEntity WRITE setKeyboardFocusEntity)
|
||||
|
||||
friend EntityPropertyMetadataRequest;
|
||||
public:
|
||||
EntityScriptingInterface(bool bidOnSimulationOwnership);
|
||||
|
||||
|
@ -212,6 +228,26 @@ public slots:
|
|||
Q_INVOKABLE RayToEntityIntersectionResult findRayIntersectionBlocking(const PickRay& ray, bool precisionPicking = false, const QScriptValue& entityIdsToInclude = QScriptValue(), const QScriptValue& entityIdsToDiscard = QScriptValue());
|
||||
|
||||
Q_INVOKABLE bool reloadServerScripts(QUuid entityID);
|
||||
|
||||
/**jsdoc
|
||||
* Query additional metadata for "magic" Entity properties like `script` and `serverScripts`.
|
||||
*
|
||||
* @function Entities.queryPropertyMetadata
|
||||
* @param {EntityID} entityID The ID of the entity.
|
||||
* @param {string} property The name of the property extended metadata is wanted for.
|
||||
* @param {ResultCallback} callback Executes callback(err, result) with the query results.
|
||||
*/
|
||||
/**jsdoc
|
||||
* Query additional metadata for "magic" Entity properties like `script` and `serverScripts`.
|
||||
*
|
||||
* @function Entities.queryPropertyMetadata
|
||||
* @param {EntityID} entityID The ID of the entity.
|
||||
* @param {string} property The name of the property extended metadata is wanted for.
|
||||
* @param {Object} thisObject The scoping "this" context that callback will be executed within.
|
||||
* @param {ResultCallback} callbackOrMethodName Executes thisObject[callbackOrMethodName](err, result) with the query results.
|
||||
*/
|
||||
Q_INVOKABLE bool queryPropertyMetadata(QUuid entityID, QScriptValue property, QScriptValue scopeOrCallback, QScriptValue methodOrName = QScriptValue());
|
||||
|
||||
Q_INVOKABLE bool getServerScriptStatus(QUuid entityID, QScriptValue callback);
|
||||
|
||||
Q_INVOKABLE void setLightsArePickable(bool value);
|
||||
|
@ -325,6 +361,11 @@ signals:
|
|||
|
||||
void webEventReceived(const EntityItemID& entityItemID, const QVariant& message);
|
||||
|
||||
protected:
|
||||
void withEntitiesScriptEngine(std::function<void(EntitiesScriptEngineProvider*)> function) {
|
||||
std::lock_guard<std::recursive_mutex> lock(_entitiesScriptEngineLock);
|
||||
function(_entitiesScriptEngine);
|
||||
};
|
||||
private:
|
||||
bool actionWorker(const QUuid& entityID, std::function<bool(EntitySimulationPointer, EntityItemPointer)> actor);
|
||||
bool polyVoxWorker(QUuid entityID, std::function<bool(PolyVoxEntityItem&)> actor);
|
||||
|
|
|
@ -1,67 +0,0 @@
|
|||
//
|
||||
// BaseScriptEngine.h
|
||||
// libraries/script-engine/src
|
||||
//
|
||||
// Created by Timothy Dedischew on 02/01/17.
|
||||
// Copyright 2017 High Fidelity, Inc.
|
||||
//
|
||||
// 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_BaseScriptEngine_h
|
||||
#define hifi_BaseScriptEngine_h
|
||||
|
||||
#include <functional>
|
||||
#include <QtCore/QDebug>
|
||||
#include <QtScript/QScriptEngine>
|
||||
|
||||
#include "SettingHandle.h"
|
||||
|
||||
// common base class for extending QScriptEngine itself
|
||||
class BaseScriptEngine : public QScriptEngine {
|
||||
Q_OBJECT
|
||||
public:
|
||||
static const QString SCRIPT_EXCEPTION_FORMAT;
|
||||
static const QString SCRIPT_BACKTRACE_SEP;
|
||||
|
||||
BaseScriptEngine() {}
|
||||
|
||||
Q_INVOKABLE QScriptValue evaluateInClosure(const QScriptValue& locals, const QScriptProgram& program);
|
||||
|
||||
Q_INVOKABLE QScriptValue lintScript(const QString& sourceCode, const QString& fileName, const int lineNumber = 1);
|
||||
Q_INVOKABLE QScriptValue makeError(const QScriptValue& other = QScriptValue(), const QString& type = "Error");
|
||||
Q_INVOKABLE QString formatException(const QScriptValue& exception);
|
||||
QScriptValue cloneUncaughtException(const QString& detail = QString());
|
||||
|
||||
signals:
|
||||
void unhandledException(const QScriptValue& exception);
|
||||
|
||||
protected:
|
||||
void _emitUnhandledException(const QScriptValue& exception);
|
||||
QScriptValue newLambdaFunction(std::function<QScriptValue(QScriptContext *context, QScriptEngine* engine)> operation, const QScriptValue& data = QScriptValue(), const QScriptEngine::ValueOwnership& ownership = QScriptEngine::AutoOwnership);
|
||||
|
||||
static const QString _SETTINGS_ENABLE_EXTENDED_EXCEPTIONS;
|
||||
Setting::Handle<bool> _enableExtendedJSExceptions { _SETTINGS_ENABLE_EXTENDED_EXCEPTIONS, true };
|
||||
#ifdef DEBUG_JS
|
||||
static void _debugDump(const QString& header, const QScriptValue& object, const QString& footer = QString());
|
||||
#endif
|
||||
};
|
||||
|
||||
// Lambda helps create callable QScriptValues out of std::functions:
|
||||
// (just meant for use from within the script engine itself)
|
||||
class Lambda : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
Lambda(QScriptEngine *engine, std::function<QScriptValue(QScriptContext *context, QScriptEngine* engine)> operation, QScriptValue data);
|
||||
~Lambda();
|
||||
public slots:
|
||||
QScriptValue call();
|
||||
QString toString() const;
|
||||
private:
|
||||
QScriptEngine* engine;
|
||||
std::function<QScriptValue(QScriptContext *context, QScriptEngine* engine)> operation;
|
||||
QScriptValue data;
|
||||
};
|
||||
|
||||
#endif // hifi_BaseScriptEngine_h
|
|
@ -19,6 +19,9 @@
|
|||
#include <QtCore/QThread>
|
||||
#include <QtCore/QRegularExpression>
|
||||
|
||||
#include <QtCore/QFuture>
|
||||
#include <QtConcurrent/QtConcurrentRun>
|
||||
|
||||
#include <QtWidgets/QMainWindow>
|
||||
#include <QtWidgets/QApplication>
|
||||
|
||||
|
@ -72,13 +75,18 @@
|
|||
|
||||
#include "MIDIEvent.h"
|
||||
|
||||
const QString ScriptEngine::_SETTINGS_ENABLE_EXTENDED_EXCEPTIONS {
|
||||
"com.highfidelity.experimental.enableExtendedJSExceptions"
|
||||
};
|
||||
|
||||
static const int MAX_MODULE_ID_LENGTH { 4096 };
|
||||
static const int MAX_DEBUG_VALUE_LENGTH { 80 };
|
||||
|
||||
static const QScriptEngine::QObjectWrapOptions DEFAULT_QOBJECT_WRAP_OPTIONS =
|
||||
QScriptEngine::ExcludeDeleteLater | QScriptEngine::ExcludeChildObjects;
|
||||
static const QScriptValue::PropertyFlags READONLY_PROP_FLAGS { QScriptValue::ReadOnly | QScriptValue::Undeletable };
|
||||
static const QScriptValue::PropertyFlags READONLY_HIDDEN_PROP_FLAGS { READONLY_PROP_FLAGS | QScriptValue::SkipInEnumeration };
|
||||
|
||||
|
||||
|
||||
static const bool HIFI_AUTOREFRESH_FILE_SCRIPTS { true };
|
||||
|
||||
Q_DECLARE_METATYPE(QScriptEngine::FunctionSignature)
|
||||
|
@ -86,7 +94,7 @@ int functionSignatureMetaID = qRegisterMetaType<QScriptEngine::FunctionSignature
|
|||
|
||||
Q_LOGGING_CATEGORY(scriptengineScript, "hifi.scriptengine.script")
|
||||
|
||||
static QScriptValue debugPrint(QScriptContext* context, QScriptEngine* engine){
|
||||
static QScriptValue debugPrint(QScriptContext* context, QScriptEngine* engine) {
|
||||
QString message = "";
|
||||
for (int i = 0; i < context->argumentCount(); i++) {
|
||||
if (i > 0) {
|
||||
|
@ -143,7 +151,7 @@ QString encodeEntityIdIntoEntityUrl(const QString& url, const QString& entityID)
|
|||
}
|
||||
|
||||
QString ScriptEngine::logException(const QScriptValue& exception) {
|
||||
auto message = formatException(exception);
|
||||
auto message = formatException(exception, _enableExtendedJSExceptions.get());
|
||||
scriptErrorMessage(message);
|
||||
return message;
|
||||
}
|
||||
|
@ -335,7 +343,7 @@ void ScriptEngine::runInThread() {
|
|||
// The thread interface cannot live on itself, and we want to move this into the thread, so
|
||||
// the thread cannot have this as a parent.
|
||||
QThread* workerThread = new QThread();
|
||||
workerThread->setObjectName(QString("Script Thread:") + getFilename());
|
||||
workerThread->setObjectName(QString("js:") + getFilename().replace("about:",""));
|
||||
moveToThread(workerThread);
|
||||
|
||||
// NOTE: If you connect any essential signals for proper shutdown or cleanup of
|
||||
|
@ -534,6 +542,40 @@ static QScriptValue createScriptableResourcePrototype(QScriptEngine* engine) {
|
|||
return prototype;
|
||||
}
|
||||
|
||||
void ScriptEngine::resetModuleCache(bool deleteScriptCache) {
|
||||
if (QThread::currentThread() != thread()) {
|
||||
executeOnScriptThread([=]() { resetModuleCache(deleteScriptCache); });
|
||||
return;
|
||||
}
|
||||
auto jsRequire = globalObject().property("Script").property("require");
|
||||
auto cache = jsRequire.property("cache");
|
||||
auto cacheMeta = jsRequire.data();
|
||||
|
||||
if (deleteScriptCache) {
|
||||
QScriptValueIterator it(cache);
|
||||
while (it.hasNext()) {
|
||||
it.next();
|
||||
if (it.flags() & QScriptValue::SkipInEnumeration) {
|
||||
continue;
|
||||
}
|
||||
qCDebug(scriptengine) << "resetModuleCache(true) -- staging " << it.name() << " for cache reset at next require";
|
||||
cacheMeta.setProperty(it.name(), true);
|
||||
}
|
||||
}
|
||||
cache = newObject();
|
||||
if (!cacheMeta.isObject()) {
|
||||
cacheMeta = newObject();
|
||||
cacheMeta.setProperty("id", "Script.require.cacheMeta");
|
||||
cacheMeta.setProperty("type", "cacheMeta");
|
||||
jsRequire.setData(cacheMeta);
|
||||
}
|
||||
cache.setProperty("__created__", (double)QDateTime::currentMSecsSinceEpoch(), QScriptValue::SkipInEnumeration);
|
||||
#if DEBUG_JS_MODULES
|
||||
cache.setProperty("__meta__", cacheMeta, READONLY_HIDDEN_PROP_FLAGS);
|
||||
#endif
|
||||
jsRequire.setProperty("cache", cache, READONLY_PROP_FLAGS);
|
||||
}
|
||||
|
||||
void ScriptEngine::init() {
|
||||
if (_isInitialized) {
|
||||
return; // only initialize once
|
||||
|
@ -587,6 +629,15 @@ void ScriptEngine::init() {
|
|||
|
||||
registerGlobalObject("Script", this);
|
||||
|
||||
{
|
||||
// set up Script.require.resolve and Script.require.cache
|
||||
auto Script = globalObject().property("Script");
|
||||
auto require = Script.property("require");
|
||||
auto resolve = Script.property("_requireResolve");
|
||||
require.setProperty("resolve", resolve, READONLY_PROP_FLAGS);
|
||||
resetModuleCache();
|
||||
}
|
||||
|
||||
registerGlobalObject("Audio", &AudioScriptingInterface::getInstance());
|
||||
registerGlobalObject("Entities", entityScriptingInterface.data());
|
||||
registerGlobalObject("Quat", &_quatLibrary);
|
||||
|
@ -859,6 +910,11 @@ void ScriptEngine::addEventHandler(const EntityItemID& entityID, const QString&
|
|||
handlersForEvent << handlerData; // Note that the same handler can be added many times. See removeEntityEventHandler().
|
||||
}
|
||||
|
||||
// this is not redundant -- the version in BaseScriptEngine is specifically not Q_INVOKABLE
|
||||
QScriptValue ScriptEngine::evaluateInClosure(const QScriptValue& closure, const QScriptProgram& program) {
|
||||
return BaseScriptEngine::evaluateInClosure(closure, program);
|
||||
}
|
||||
|
||||
QScriptValue ScriptEngine::evaluate(const QString& sourceCode, const QString& fileName, int lineNumber) {
|
||||
if (DependencyManager::get<ScriptEngines>()->isStopped()) {
|
||||
return QScriptValue(); // bail early
|
||||
|
@ -881,29 +937,26 @@ QScriptValue ScriptEngine::evaluate(const QString& sourceCode, const QString& fi
|
|||
// Check syntax
|
||||
auto syntaxError = lintScript(sourceCode, fileName);
|
||||
if (syntaxError.isError()) {
|
||||
if (isEvaluating()) {
|
||||
currentContext()->throwValue(syntaxError);
|
||||
} else {
|
||||
if (!isEvaluating()) {
|
||||
syntaxError.setProperty("detail", "evaluate");
|
||||
emit unhandledException(syntaxError);
|
||||
}
|
||||
raiseException(syntaxError);
|
||||
maybeEmitUncaughtException("lint");
|
||||
return syntaxError;
|
||||
}
|
||||
QScriptProgram program { sourceCode, fileName, lineNumber };
|
||||
if (program.isNull()) {
|
||||
// can this happen?
|
||||
auto err = makeError("could not create QScriptProgram for " + fileName);
|
||||
emit unhandledException(err);
|
||||
raiseException(err);
|
||||
maybeEmitUncaughtException("compile");
|
||||
return err;
|
||||
}
|
||||
|
||||
QScriptValue result;
|
||||
{
|
||||
result = BaseScriptEngine::evaluate(program);
|
||||
if (!isEvaluating() && hasUncaughtException()) {
|
||||
emit unhandledException(cloneUncaughtException(__FUNCTION__));
|
||||
clearExceptions();
|
||||
}
|
||||
maybeEmitUncaughtException("evaluate");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
@ -926,10 +979,7 @@ void ScriptEngine::run() {
|
|||
|
||||
{
|
||||
evaluate(_scriptContents, _fileNameString);
|
||||
if (!isEvaluating() && hasUncaughtException()) {
|
||||
emit unhandledException(cloneUncaughtException(__FUNCTION__));
|
||||
clearExceptions();
|
||||
}
|
||||
maybeEmitUncaughtException(__FUNCTION__);
|
||||
}
|
||||
#ifdef _WIN32
|
||||
// VS13 does not sleep_until unless it uses the system_clock, see:
|
||||
|
@ -1300,11 +1350,361 @@ void ScriptEngine::print(const QString& message) {
|
|||
emit printedMessage(message);
|
||||
}
|
||||
|
||||
// Script.require.resolve -- like resolvePath, but performs more validation and throws exceptions on invalid module identifiers (for consistency with Node.js)
|
||||
QString ScriptEngine::_requireResolve(const QString& moduleId, const QString& relativeTo) {
|
||||
if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
|
||||
return QString();
|
||||
}
|
||||
QUrl defaultScriptsLoc = defaultScriptsLocation();
|
||||
QUrl url(moduleId);
|
||||
|
||||
auto displayId = moduleId;
|
||||
if (displayId.length() > MAX_DEBUG_VALUE_LENGTH) {
|
||||
displayId = displayId.mid(0, MAX_DEBUG_VALUE_LENGTH) + "...";
|
||||
}
|
||||
auto message = QString("Cannot find module '%1' (%2)").arg(displayId);
|
||||
|
||||
auto throwResolveError = [&](const QScriptValue& error) -> QString {
|
||||
raiseException(error);
|
||||
maybeEmitUncaughtException("require.resolve");
|
||||
return QString();
|
||||
};
|
||||
|
||||
// de-fuzz the input a little by restricting to rational sizes
|
||||
auto idLength = url.toString().length();
|
||||
if (idLength < 1 || idLength > MAX_MODULE_ID_LENGTH) {
|
||||
auto details = QString("rejecting invalid module id size (%1 chars [1,%2])")
|
||||
.arg(idLength).arg(MAX_MODULE_ID_LENGTH);
|
||||
return throwResolveError(makeError(message.arg(details), "RangeError"));
|
||||
}
|
||||
|
||||
// this regex matches: absolute, dotted or path-like URLs
|
||||
// (ie: the kind of stuff ScriptEngine::resolvePath already handles)
|
||||
QRegularExpression qualified ("^\\w+:|^/|^[.]{1,2}(/|$)");
|
||||
|
||||
// this is for module.require (which is a bound version of require that's always relative to the module path)
|
||||
if (!relativeTo.isEmpty()) {
|
||||
url = QUrl(relativeTo).resolved(moduleId);
|
||||
url = resolvePath(url.toString());
|
||||
} else if (qualified.match(moduleId).hasMatch()) {
|
||||
url = resolvePath(moduleId);
|
||||
} else {
|
||||
// check if the moduleId refers to a "system" module
|
||||
QString systemPath = defaultScriptsLoc.path();
|
||||
QString systemModulePath = QString("%1/modules/%2.js").arg(systemPath).arg(moduleId);
|
||||
url = defaultScriptsLoc;
|
||||
url.setPath(systemModulePath);
|
||||
if (!QFileInfo(url.toLocalFile()).isFile()) {
|
||||
if (!moduleId.contains("./")) {
|
||||
// the user might be trying to refer to a relative file without anchoring it
|
||||
// let's do them a favor and test for that case -- offering specific advice if detected
|
||||
auto unanchoredUrl = resolvePath("./" + moduleId);
|
||||
if (QFileInfo(unanchoredUrl.toLocalFile()).isFile()) {
|
||||
auto msg = QString("relative module ids must be anchored; use './%1' instead")
|
||||
.arg(moduleId);
|
||||
return throwResolveError(makeError(message.arg(msg)));
|
||||
}
|
||||
}
|
||||
return throwResolveError(makeError(message.arg("system module not found")));
|
||||
}
|
||||
}
|
||||
|
||||
if (url.isRelative()) {
|
||||
return throwResolveError(makeError(message.arg("could not resolve module id")));
|
||||
}
|
||||
|
||||
// if it looks like a local file, verify that it's an allowed path and really a file
|
||||
if (url.isLocalFile()) {
|
||||
QFileInfo file(url.toLocalFile());
|
||||
QUrl canonical = url;
|
||||
if (file.exists()) {
|
||||
canonical.setPath(file.canonicalFilePath());
|
||||
}
|
||||
|
||||
bool disallowOutsideFiles = !defaultScriptsLocation().isParentOf(canonical) && !currentSandboxURL.isLocalFile();
|
||||
if (disallowOutsideFiles && !PathUtils::isDescendantOf(canonical, currentSandboxURL)) {
|
||||
return throwResolveError(makeError(message.arg(
|
||||
QString("path '%1' outside of origin script '%2' '%3'")
|
||||
.arg(PathUtils::stripFilename(url))
|
||||
.arg(PathUtils::stripFilename(currentSandboxURL))
|
||||
.arg(canonical.toString())
|
||||
)));
|
||||
}
|
||||
if (!file.exists()) {
|
||||
return throwResolveError(makeError(message.arg("path does not exist: " + url.toLocalFile())));
|
||||
}
|
||||
if (!file.isFile()) {
|
||||
return throwResolveError(makeError(message.arg("path is not a file: " + url.toLocalFile())));
|
||||
}
|
||||
}
|
||||
|
||||
maybeEmitUncaughtException(__FUNCTION__);
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
// retrieves the current parent module from the JS scope chain
|
||||
QScriptValue ScriptEngine::currentModule() {
|
||||
if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
|
||||
return unboundNullValue();
|
||||
}
|
||||
auto jsRequire = globalObject().property("Script").property("require");
|
||||
auto cache = jsRequire.property("cache");
|
||||
auto candidate = QScriptValue();
|
||||
for (auto c = currentContext(); c && !candidate.isObject(); c = c->parentContext()) {
|
||||
QScriptContextInfo contextInfo { c };
|
||||
candidate = cache.property(contextInfo.fileName());
|
||||
}
|
||||
if (!candidate.isObject()) {
|
||||
return QScriptValue();
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
|
||||
// replaces or adds "module" to "parent.children[]" array
|
||||
// (for consistency with Node.js and userscript cache invalidation without "cache busters")
|
||||
bool ScriptEngine::registerModuleWithParent(const QScriptValue& module, const QScriptValue& parent) {
|
||||
auto children = parent.property("children");
|
||||
if (children.isArray()) {
|
||||
auto key = module.property("id");
|
||||
auto length = children.property("length").toInt32();
|
||||
for (int i = 0; i < length; i++) {
|
||||
if (children.property(i).property("id").strictlyEquals(key)) {
|
||||
qCDebug(scriptengine_module) << key.toString() << " updating parent.children[" << i << "] = module";
|
||||
children.setProperty(i, module);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
qCDebug(scriptengine_module) << key.toString() << " appending parent.children[" << length << "] = module";
|
||||
children.setProperty(length, module);
|
||||
return true;
|
||||
} else if (parent.isValid()) {
|
||||
qCDebug(scriptengine_module) << "registerModuleWithParent -- unrecognized parent" << parent.toVariant().toString();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// creates a new JS "module" Object with default metadata properties
|
||||
QScriptValue ScriptEngine::newModule(const QString& modulePath, const QScriptValue& parent) {
|
||||
auto closure = newObject();
|
||||
auto exports = newObject();
|
||||
auto module = newObject();
|
||||
qCDebug(scriptengine_module) << "newModule" << modulePath << parent.property("filename").toString();
|
||||
|
||||
closure.setProperty("module", module, READONLY_PROP_FLAGS);
|
||||
|
||||
// note: this becomes the "exports" free variable, so should not be set read only
|
||||
closure.setProperty("exports", exports);
|
||||
|
||||
// make the closure available to module instantiation
|
||||
module.setProperty("__closure__", closure, READONLY_HIDDEN_PROP_FLAGS);
|
||||
|
||||
// for consistency with Node.js Module
|
||||
module.setProperty("id", modulePath, READONLY_PROP_FLAGS);
|
||||
module.setProperty("filename", modulePath, READONLY_PROP_FLAGS);
|
||||
module.setProperty("exports", exports); // not readonly
|
||||
module.setProperty("loaded", false, READONLY_PROP_FLAGS);
|
||||
module.setProperty("parent", parent, READONLY_PROP_FLAGS);
|
||||
module.setProperty("children", newArray(), READONLY_PROP_FLAGS);
|
||||
|
||||
// module.require is a bound version of require that always resolves relative to that module's path
|
||||
auto boundRequire = QScriptEngine::evaluate("(function(id) { return Script.require(Script.require.resolve(id, this.filename)); })", "(boundRequire)");
|
||||
module.setProperty("require", boundRequire, READONLY_PROP_FLAGS);
|
||||
|
||||
return module;
|
||||
}
|
||||
|
||||
// synchronously fetch a module's source code using BatchLoader
|
||||
QVariantMap ScriptEngine::fetchModuleSource(const QString& modulePath, const bool forceDownload) {
|
||||
using UrlMap = QMap<QUrl, QString>;
|
||||
auto scriptCache = DependencyManager::get<ScriptCache>();
|
||||
QVariantMap req;
|
||||
qCDebug(scriptengine_module) << "require.fetchModuleSource: " << QUrl(modulePath).fileName() << QThread::currentThread();
|
||||
|
||||
auto onload = [=, &req](const UrlMap& data, const UrlMap& _status) {
|
||||
auto url = modulePath;
|
||||
auto status = _status[url];
|
||||
auto contents = data[url];
|
||||
qCDebug(scriptengine_module) << "require.fetchModuleSource.onload: " << QUrl(url).fileName() << status << QThread::currentThread();
|
||||
if (isStopping()) {
|
||||
req["status"] = "Stopped";
|
||||
req["success"] = false;
|
||||
} else {
|
||||
req["url"] = url;
|
||||
req["status"] = status;
|
||||
req["success"] = ScriptCache::isSuccessStatus(status);
|
||||
req["contents"] = contents;
|
||||
}
|
||||
};
|
||||
|
||||
if (forceDownload) {
|
||||
qCDebug(scriptengine_module) << "require.requestScript -- clearing cache for" << modulePath;
|
||||
scriptCache->deleteScript(modulePath);
|
||||
}
|
||||
BatchLoader* loader = new BatchLoader(QList<QUrl>({ modulePath }));
|
||||
connect(loader, &BatchLoader::finished, this, onload);
|
||||
connect(this, &QObject::destroyed, loader, &QObject::deleteLater);
|
||||
// fail faster? (since require() blocks the engine thread while resolving dependencies)
|
||||
const int MAX_RETRIES = 1;
|
||||
|
||||
loader->start(MAX_RETRIES);
|
||||
|
||||
if (!loader->isFinished()) {
|
||||
QTimer monitor;
|
||||
QEventLoop loop;
|
||||
QObject::connect(loader, &BatchLoader::finished, this, [this, &monitor, &loop]{
|
||||
monitor.stop();
|
||||
loop.quit();
|
||||
});
|
||||
|
||||
// this helps detect the case where stop() is invoked during the download
|
||||
// but not seen in time to abort processing in onload()...
|
||||
connect(&monitor, &QTimer::timeout, this, [this, &loop, &loader]{
|
||||
if (isStopping()) {
|
||||
loop.exit(-1);
|
||||
}
|
||||
});
|
||||
monitor.start(500);
|
||||
loop.exec();
|
||||
}
|
||||
loader->deleteLater();
|
||||
return req;
|
||||
}
|
||||
|
||||
// evaluate a pending module object using the fetched source code
|
||||
QScriptValue ScriptEngine::instantiateModule(const QScriptValue& module, const QString& sourceCode) {
|
||||
QScriptValue result;
|
||||
auto modulePath = module.property("filename").toString();
|
||||
auto closure = module.property("__closure__");
|
||||
|
||||
qCDebug(scriptengine_module) << QString("require.instantiateModule: %1 / %2 bytes")
|
||||
.arg(QUrl(modulePath).fileName()).arg(sourceCode.length());
|
||||
|
||||
if (module.property("content-type").toString() == "application/json") {
|
||||
qCDebug(scriptengine_module) << "... parsing as JSON";
|
||||
closure.setProperty("__json", sourceCode);
|
||||
result = evaluateInClosure(closure, { "module.exports = JSON.parse(__json)", modulePath });
|
||||
} else {
|
||||
// scoped vars for consistency with Node.js
|
||||
closure.setProperty("require", module.property("require"));
|
||||
closure.setProperty("__filename", modulePath, READONLY_HIDDEN_PROP_FLAGS);
|
||||
closure.setProperty("__dirname", QString(modulePath).replace(QRegExp("/[^/]*$"), ""), READONLY_HIDDEN_PROP_FLAGS);
|
||||
result = evaluateInClosure(closure, { sourceCode, modulePath });
|
||||
}
|
||||
maybeEmitUncaughtException(__FUNCTION__);
|
||||
return result;
|
||||
}
|
||||
|
||||
// CommonJS/Node.js like require/module support
|
||||
QScriptValue ScriptEngine::require(const QString& moduleId) {
|
||||
qCDebug(scriptengine_module) << "ScriptEngine::require(" << moduleId.left(MAX_DEBUG_VALUE_LENGTH) << ")";
|
||||
if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
|
||||
return unboundNullValue();
|
||||
}
|
||||
|
||||
auto jsRequire = globalObject().property("Script").property("require");
|
||||
auto cacheMeta = jsRequire.data();
|
||||
auto cache = jsRequire.property("cache");
|
||||
auto parent = currentModule();
|
||||
|
||||
auto throwModuleError = [&](const QString& modulePath, const QScriptValue& error) {
|
||||
cache.setProperty(modulePath, nullValue());
|
||||
if (!error.isNull()) {
|
||||
#ifdef DEBUG_JS_MODULES
|
||||
qCWarning(scriptengine_module) << "throwing module error:" << error.toString() << modulePath << error.property("stack").toString();
|
||||
#endif
|
||||
raiseException(error);
|
||||
}
|
||||
maybeEmitUncaughtException("module");
|
||||
return unboundNullValue();
|
||||
};
|
||||
|
||||
// start by resolving the moduleId into a fully-qualified path/URL
|
||||
QString modulePath = _requireResolve(moduleId);
|
||||
if (modulePath.isNull() || hasUncaughtException()) {
|
||||
// the resolver already threw an exception -- bail early
|
||||
maybeEmitUncaughtException(__FUNCTION__);
|
||||
return unboundNullValue();
|
||||
}
|
||||
|
||||
// check the resolved path against the cache
|
||||
auto module = cache.property(modulePath);
|
||||
|
||||
// modules get cached in `Script.require.cache` and (similar to Node.js) users can access it
|
||||
// to inspect particular entries and invalidate them by deleting the key:
|
||||
// `delete Script.require.cache[Script.require.resolve(moduleId)];`
|
||||
|
||||
// cacheMeta is just used right now to tell deleted keys apart from undefined ones
|
||||
bool invalidateCache = module.isUndefined() && cacheMeta.property(moduleId).isValid();
|
||||
|
||||
// reset the cacheMeta record so invalidation won't apply next time, even if the module fails to load
|
||||
cacheMeta.setProperty(modulePath, QScriptValue());
|
||||
|
||||
auto exports = module.property("exports");
|
||||
if (!invalidateCache && exports.isObject()) {
|
||||
// we have found a cached module -- just need to possibly register it with current parent
|
||||
qCDebug(scriptengine_module) << QString("require - using cached module '%1' for '%2' (loaded: %3)")
|
||||
.arg(modulePath).arg(moduleId).arg(module.property("loaded").toString());
|
||||
registerModuleWithParent(module, parent);
|
||||
maybeEmitUncaughtException("cached module");
|
||||
return exports;
|
||||
}
|
||||
|
||||
// bootstrap / register new empty module
|
||||
module = newModule(modulePath, parent);
|
||||
registerModuleWithParent(module, parent);
|
||||
|
||||
// add it to the cache (this is done early so any cyclic dependencies pick up)
|
||||
cache.setProperty(modulePath, module);
|
||||
|
||||
// download the module source
|
||||
auto req = fetchModuleSource(modulePath, invalidateCache);
|
||||
|
||||
if (!req.contains("success") || !req["success"].toBool()) {
|
||||
auto error = QString("error retrieving script (%1)").arg(req["status"].toString());
|
||||
return throwModuleError(modulePath, error);
|
||||
}
|
||||
|
||||
#if DEBUG_JS_MODULES
|
||||
qCDebug(scriptengine_module) << "require.loaded: " <<
|
||||
QUrl(req["url"].toString()).fileName() << req["status"].toString();
|
||||
#endif
|
||||
|
||||
auto sourceCode = req["contents"].toString();
|
||||
|
||||
if (QUrl(modulePath).fileName().endsWith(".json", Qt::CaseInsensitive)) {
|
||||
module.setProperty("content-type", "application/json");
|
||||
} else {
|
||||
module.setProperty("content-type", "application/javascript");
|
||||
}
|
||||
|
||||
// evaluate the module
|
||||
auto result = instantiateModule(module, sourceCode);
|
||||
|
||||
if (result.isError() && !result.strictlyEquals(module.property("exports"))) {
|
||||
qCWarning(scriptengine_module) << "-- result.isError --" << result.toString();
|
||||
return throwModuleError(modulePath, result);
|
||||
}
|
||||
|
||||
// mark as fully-loaded
|
||||
module.setProperty("loaded", true, READONLY_PROP_FLAGS);
|
||||
|
||||
// set up a new reference point for detecting cache key deletion
|
||||
cacheMeta.setProperty(modulePath, module);
|
||||
|
||||
qCDebug(scriptengine_module) << "//ScriptEngine::require(" << moduleId << ")";
|
||||
|
||||
maybeEmitUncaughtException(__FUNCTION__);
|
||||
return module.property("exports");
|
||||
}
|
||||
|
||||
// If a callback is specified, the included files will be loaded asynchronously and the callback will be called
|
||||
// when all of the files have finished loading.
|
||||
// If no callback is specified, the included files will be loaded synchronously and will block execution until
|
||||
// all of the files have finished loading.
|
||||
void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callback) {
|
||||
if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
|
||||
return;
|
||||
}
|
||||
if (DependencyManager::get<ScriptEngines>()->isStopped()) {
|
||||
scriptWarningMessage("Script.include() while shutting down is ignored... includeFiles:"
|
||||
+ includeFiles.join(",") + "parent script:" + getFilename());
|
||||
|
@ -1367,7 +1767,7 @@ void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callbac
|
|||
|
||||
doWithEnvironment(capturedEntityIdentifier, capturedSandboxURL, operation);
|
||||
if (hasUncaughtException()) {
|
||||
emit unhandledException(cloneUncaughtException(__FUNCTION__));
|
||||
emit unhandledException(cloneUncaughtException("evaluateInclude"));
|
||||
clearExceptions();
|
||||
}
|
||||
} else {
|
||||
|
@ -1414,6 +1814,9 @@ void ScriptEngine::include(const QString& includeFile, QScriptValue callback) {
|
|||
// as a stand-alone script. To accomplish this, the ScriptEngine class just emits a signal which
|
||||
// the Application or other context will connect to in order to know to actually load the script
|
||||
void ScriptEngine::load(const QString& loadFile) {
|
||||
if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
|
||||
return;
|
||||
}
|
||||
if (DependencyManager::get<ScriptEngines>()->isStopped()) {
|
||||
scriptWarningMessage("Script.load() while shutting down is ignored... loadFile:"
|
||||
+ loadFile + "parent script:" + getFilename());
|
||||
|
@ -1483,6 +1886,52 @@ void ScriptEngine::updateEntityScriptStatus(const EntityItemID& entityID, const
|
|||
emit entityScriptDetailsUpdated();
|
||||
}
|
||||
|
||||
QVariant ScriptEngine::cloneEntityScriptDetails(const EntityItemID& entityID) {
|
||||
static const QVariant NULL_VARIANT { qVariantFromValue((QObject*)nullptr) };
|
||||
QVariantMap map;
|
||||
if (entityID.isNull()) {
|
||||
// TODO: find better way to report JS Error across thread/process boundaries
|
||||
map["isError"] = true;
|
||||
map["errorInfo"] = "Error: getEntityScriptDetails -- invalid entityID";
|
||||
} else {
|
||||
#ifdef DEBUG_ENTITY_STATES
|
||||
qDebug() << "cloneEntityScriptDetails" << entityID << QThread::currentThread();
|
||||
#endif
|
||||
EntityScriptDetails scriptDetails;
|
||||
if (getEntityScriptDetails(entityID, scriptDetails)) {
|
||||
#ifdef DEBUG_ENTITY_STATES
|
||||
qDebug() << "gotEntityScriptDetails" << scriptDetails.status << QThread::currentThread();
|
||||
#endif
|
||||
map["isRunning"] = isEntityScriptRunning(entityID);
|
||||
map["status"] = EntityScriptStatus_::valueToKey(scriptDetails.status).toLower();
|
||||
map["errorInfo"] = scriptDetails.errorInfo;
|
||||
map["entityID"] = entityID.toString();
|
||||
#ifdef DEBUG_ENTITY_STATES
|
||||
{
|
||||
auto debug = QVariantMap();
|
||||
debug["script"] = scriptDetails.scriptText;
|
||||
debug["scriptObject"] = scriptDetails.scriptObject.toVariant();
|
||||
debug["lastModified"] = (qlonglong)scriptDetails.lastModified;
|
||||
debug["sandboxURL"] = scriptDetails.definingSandboxURL;
|
||||
map["debug"] = debug;
|
||||
}
|
||||
#endif
|
||||
} else {
|
||||
#ifdef DEBUG_ENTITY_STATES
|
||||
qDebug() << "!gotEntityScriptDetails" << QThread::currentThread();
|
||||
#endif
|
||||
map["isError"] = true;
|
||||
map["errorInfo"] = "Entity script details unavailable";
|
||||
map["entityID"] = entityID.toString();
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
QFuture<QVariant> ScriptEngine::getLocalEntityScriptDetails(const EntityItemID& entityID) {
|
||||
return QtConcurrent::run(this, &ScriptEngine::cloneEntityScriptDetails, entityID);
|
||||
}
|
||||
|
||||
bool ScriptEngine::getEntityScriptDetails(const EntityItemID& entityID, EntityScriptDetails &details) const {
|
||||
auto it = _entityScripts.constFind(entityID);
|
||||
if (it == _entityScripts.constEnd()) {
|
||||
|
@ -1621,10 +2070,10 @@ void ScriptEngine::loadEntityScript(const EntityItemID& entityID, const QString&
|
|||
|
||||
auto scriptCache = DependencyManager::get<ScriptCache>();
|
||||
// note: see EntityTreeRenderer.cpp for shared pointer lifecycle management
|
||||
QWeakPointer<ScriptEngine> weakRef(sharedFromThis());
|
||||
QWeakPointer<BaseScriptEngine> weakRef(sharedFromThis());
|
||||
scriptCache->getScriptContents(entityScript,
|
||||
[this, weakRef, entityScript, entityID](const QString& url, const QString& contents, bool isURL, bool success, const QString& status) {
|
||||
QSharedPointer<ScriptEngine> strongRef(weakRef);
|
||||
QSharedPointer<BaseScriptEngine> strongRef(weakRef);
|
||||
if (!strongRef) {
|
||||
qCWarning(scriptengine) << "loadEntityScript.contentAvailable -- ScriptEngine was deleted during getScriptContents!!";
|
||||
return;
|
||||
|
@ -1743,13 +2192,12 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co
|
|||
timeout.setSingleShot(true);
|
||||
timeout.start(SANDBOX_TIMEOUT);
|
||||
connect(&timeout, &QTimer::timeout, [&sandbox, SANDBOX_TIMEOUT, scriptOrURL]{
|
||||
auto context = sandbox.currentContext();
|
||||
if (context) {
|
||||
qCDebug(scriptengine) << "ScriptEngine::entityScriptContentAvailable timeout(" << scriptOrURL << ")";
|
||||
|
||||
// Guard against infinite loops and non-performant code
|
||||
context->throwError(QString("Timed out (entity constructors are limited to %1ms)").arg(SANDBOX_TIMEOUT));
|
||||
}
|
||||
sandbox.raiseException(
|
||||
sandbox.makeError(QString("Timed out (entity constructors are limited to %1ms)").arg(SANDBOX_TIMEOUT))
|
||||
);
|
||||
});
|
||||
|
||||
testConstructor = sandbox.evaluate(program);
|
||||
|
@ -1765,7 +2213,7 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co
|
|||
if (exception.isError()) {
|
||||
// create a local copy using makeError to decouple from the sandbox engine
|
||||
exception = makeError(exception);
|
||||
setError(formatException(exception), EntityScriptStatus::ERROR_RUNNING_SCRIPT);
|
||||
setError(formatException(exception, _enableExtendedJSExceptions.get()), EntityScriptStatus::ERROR_RUNNING_SCRIPT);
|
||||
emit unhandledException(exception);
|
||||
return;
|
||||
}
|
||||
|
@ -1777,9 +2225,8 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co
|
|||
testConstructorType = "empty";
|
||||
}
|
||||
QString testConstructorValue = testConstructor.toString();
|
||||
const int maxTestConstructorValueSize = 80;
|
||||
if (testConstructorValue.size() > maxTestConstructorValueSize) {
|
||||
testConstructorValue = testConstructorValue.mid(0, maxTestConstructorValueSize) + "...";
|
||||
if (testConstructorValue.size() > MAX_DEBUG_VALUE_LENGTH) {
|
||||
testConstructorValue = testConstructorValue.mid(0, MAX_DEBUG_VALUE_LENGTH) + "...";
|
||||
}
|
||||
auto message = QString("failed to load entity script -- expected a function, got %1, %2")
|
||||
.arg(testConstructorType).arg(testConstructorValue);
|
||||
|
@ -1817,7 +2264,7 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co
|
|||
|
||||
if (entityScriptObject.isError()) {
|
||||
auto exception = entityScriptObject;
|
||||
setError(formatException(exception), EntityScriptStatus::ERROR_RUNNING_SCRIPT);
|
||||
setError(formatException(exception, _enableExtendedJSExceptions.get()), EntityScriptStatus::ERROR_RUNNING_SCRIPT);
|
||||
emit unhandledException(exception);
|
||||
return;
|
||||
}
|
||||
|
@ -1861,10 +2308,12 @@ void ScriptEngine::unloadEntityScript(const EntityItemID& entityID, bool shouldR
|
|||
const EntityScriptDetails &oldDetails = _entityScripts[entityID];
|
||||
if (isEntityScriptRunning(entityID)) {
|
||||
callEntityScriptMethod(entityID, "unload");
|
||||
} else {
|
||||
}
|
||||
#ifdef DEBUG_ENTITY_STATES
|
||||
else {
|
||||
qCDebug(scriptengine) << "unload called while !running" << entityID << oldDetails.status;
|
||||
}
|
||||
|
||||
#endif
|
||||
if (shouldRemoveFromMap) {
|
||||
// this was a deleted entity, we've been asked to remove it from the map
|
||||
_entityScripts.remove(entityID);
|
||||
|
@ -1956,10 +2405,7 @@ void ScriptEngine::doWithEnvironment(const EntityItemID& entityID, const QUrl& s
|
|||
#else
|
||||
operation();
|
||||
#endif
|
||||
if (!isEvaluating() && hasUncaughtException()) {
|
||||
emit unhandledException(cloneUncaughtException(!entityID.isNull() ? entityID.toString() : __FUNCTION__));
|
||||
clearExceptions();
|
||||
}
|
||||
maybeEmitUncaughtException(!entityID.isNull() ? entityID.toString() : __FUNCTION__);
|
||||
currentEntityIdentifier = oldIdentifier;
|
||||
currentSandboxURL = oldSandboxURL;
|
||||
}
|
||||
|
|
|
@ -41,6 +41,7 @@
|
|||
#include "ScriptCache.h"
|
||||
#include "ScriptUUID.h"
|
||||
#include "Vec3.h"
|
||||
#include "SettingHandle.h"
|
||||
|
||||
class QScriptEngineDebugger;
|
||||
|
||||
|
@ -78,7 +79,7 @@ public:
|
|||
QUrl definingSandboxURL { QUrl("about:EntityScript") };
|
||||
};
|
||||
|
||||
class ScriptEngine : public BaseScriptEngine, public EntitiesScriptEngineProvider, public QEnableSharedFromThis<ScriptEngine> {
|
||||
class ScriptEngine : public BaseScriptEngine, public EntitiesScriptEngineProvider {
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(QString context READ getContext)
|
||||
public:
|
||||
|
@ -137,6 +138,8 @@ public:
|
|||
/// evaluate some code in the context of the ScriptEngine and return the result
|
||||
Q_INVOKABLE QScriptValue evaluate(const QString& program, const QString& fileName, int lineNumber = 1); // this is also used by the script tool widget
|
||||
|
||||
Q_INVOKABLE QScriptValue evaluateInClosure(const QScriptValue& locals, const QScriptProgram& program);
|
||||
|
||||
/// if the script engine is not already running, this will download the URL and start the process of seting it up
|
||||
/// to run... NOTE - this is used by Application currently to load the url. We don't really want it to be exposed
|
||||
/// to scripts. we may not need this to be invokable
|
||||
|
@ -157,6 +160,16 @@ public:
|
|||
Q_INVOKABLE void include(const QStringList& includeFiles, QScriptValue callback = QScriptValue());
|
||||
Q_INVOKABLE void include(const QString& includeFile, QScriptValue callback = QScriptValue());
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// MODULE related methods
|
||||
Q_INVOKABLE QScriptValue require(const QString& moduleId);
|
||||
Q_INVOKABLE void resetModuleCache(bool deleteScriptCache = false);
|
||||
QScriptValue currentModule();
|
||||
bool registerModuleWithParent(const QScriptValue& module, const QScriptValue& parent);
|
||||
QScriptValue newModule(const QString& modulePath, const QScriptValue& parent = QScriptValue());
|
||||
QVariantMap fetchModuleSource(const QString& modulePath, const bool forceDownload = false);
|
||||
QScriptValue instantiateModule(const QScriptValue& module, const QString& sourceCode);
|
||||
|
||||
Q_INVOKABLE QObject* setInterval(const QScriptValue& function, int intervalMS);
|
||||
Q_INVOKABLE QObject* setTimeout(const QScriptValue& function, int timeoutMS);
|
||||
Q_INVOKABLE void clearInterval(QObject* timer) { stopTimer(reinterpret_cast<QTimer*>(timer)); }
|
||||
|
@ -170,6 +183,8 @@ public:
|
|||
Q_INVOKABLE bool isEntityScriptRunning(const EntityItemID& entityID) {
|
||||
return _entityScripts.contains(entityID) && _entityScripts[entityID].status == EntityScriptStatus::RUNNING;
|
||||
}
|
||||
QVariant cloneEntityScriptDetails(const EntityItemID& entityID);
|
||||
QFuture<QVariant> getLocalEntityScriptDetails(const EntityItemID& entityID) override;
|
||||
Q_INVOKABLE void loadEntityScript(const EntityItemID& entityID, const QString& entityScript, bool forceRedownload);
|
||||
Q_INVOKABLE void unloadEntityScript(const EntityItemID& entityID, bool shouldRemoveFromMap = false); // will call unload method
|
||||
Q_INVOKABLE void unloadAllEntityScripts();
|
||||
|
@ -237,6 +252,9 @@ signals:
|
|||
protected:
|
||||
void init();
|
||||
Q_INVOKABLE void executeOnScriptThread(std::function<void()> function, const Qt::ConnectionType& type = Qt::QueuedConnection );
|
||||
// note: this is not meant to be called directly, but just to have QMetaObject take care of wiring it up in general;
|
||||
// then inside of init() we just have to do "Script.require.resolve = Script._requireResolve;"
|
||||
Q_INVOKABLE QString _requireResolve(const QString& moduleId, const QString& relativeTo = QString());
|
||||
|
||||
QString logException(const QScriptValue& exception);
|
||||
void timerFired();
|
||||
|
@ -290,11 +308,16 @@ protected:
|
|||
|
||||
AssetScriptingInterface _assetScriptingInterface{ this };
|
||||
|
||||
std::function<bool()> _emitScriptUpdates{ [](){ return true; } };
|
||||
std::function<bool()> _emitScriptUpdates{ []() { return true; } };
|
||||
|
||||
std::recursive_mutex _lock;
|
||||
|
||||
std::chrono::microseconds _totalTimerExecution { 0 };
|
||||
|
||||
static const QString _SETTINGS_ENABLE_EXTENDED_MODULE_COMPAT;
|
||||
static const QString _SETTINGS_ENABLE_EXTENDED_EXCEPTIONS;
|
||||
|
||||
Setting::Handle<bool> _enableExtendedJSExceptions { _SETTINGS_ENABLE_EXTENDED_EXCEPTIONS, true };
|
||||
};
|
||||
|
||||
#endif // hifi_ScriptEngine_h
|
||||
|
|
|
@ -12,3 +12,4 @@
|
|||
#include "ScriptEngineLogging.h"
|
||||
|
||||
Q_LOGGING_CATEGORY(scriptengine, "hifi.scriptengine")
|
||||
Q_LOGGING_CATEGORY(scriptengine_module, "hifi.scriptengine.module")
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
#include <QLoggingCategory>
|
||||
|
||||
Q_DECLARE_LOGGING_CATEGORY(scriptengine)
|
||||
Q_DECLARE_LOGGING_CATEGORY(scriptengine_module)
|
||||
|
||||
#endif // hifi_ScriptEngineLogging_h
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
//
|
||||
|
||||
#include "BaseScriptEngine.h"
|
||||
#include "SharedLogging.h"
|
||||
|
||||
#include <QtCore/QString>
|
||||
#include <QtCore/QThread>
|
||||
|
@ -18,18 +19,27 @@
|
|||
#include <QtScript/QScriptValueIterator>
|
||||
#include <QtScript/QScriptContextInfo>
|
||||
|
||||
#include "ScriptEngineLogging.h"
|
||||
#include "Profile.h"
|
||||
|
||||
const QString BaseScriptEngine::_SETTINGS_ENABLE_EXTENDED_EXCEPTIONS {
|
||||
"com.highfidelity.experimental.enableExtendedJSExceptions"
|
||||
};
|
||||
|
||||
const QString BaseScriptEngine::SCRIPT_EXCEPTION_FORMAT { "[%0] %1 in %2:%3" };
|
||||
const QString BaseScriptEngine::SCRIPT_BACKTRACE_SEP { "\n " };
|
||||
|
||||
bool BaseScriptEngine::IS_THREADSAFE_INVOCATION(const QThread *thread, const QString& method) {
|
||||
if (QThread::currentThread() == thread) {
|
||||
return true;
|
||||
}
|
||||
qCCritical(shared) << QString("Scripting::%1 @ %2 -- ignoring thread-unsafe call from %3")
|
||||
.arg(method).arg(thread ? thread->objectName() : "(!thread)").arg(QThread::currentThread()->objectName());
|
||||
qCDebug(shared) << "(please resolve on the calling side by using invokeMethod, executeOnScriptThread, etc.)";
|
||||
Q_ASSERT(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
// engine-aware JS Error copier and factory
|
||||
QScriptValue BaseScriptEngine::makeError(const QScriptValue& _other, const QString& type) {
|
||||
if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
|
||||
return unboundNullValue();
|
||||
}
|
||||
auto other = _other;
|
||||
if (other.isString()) {
|
||||
other = newObject();
|
||||
|
@ -41,7 +51,7 @@ QScriptValue BaseScriptEngine::makeError(const QScriptValue& _other, const QStri
|
|||
}
|
||||
if (!proto.isFunction()) {
|
||||
#ifdef DEBUG_JS_EXCEPTIONS
|
||||
qCDebug(scriptengine) << "BaseScriptEngine::makeError -- couldn't find constructor for" << type << " -- using Error instead";
|
||||
qCDebug(shared) << "BaseScriptEngine::makeError -- couldn't find constructor for" << type << " -- using Error instead";
|
||||
#endif
|
||||
proto = globalObject().property("Error");
|
||||
}
|
||||
|
@ -64,6 +74,9 @@ QScriptValue BaseScriptEngine::makeError(const QScriptValue& _other, const QStri
|
|||
|
||||
// check syntax and when there are issues returns an actual "SyntaxError" with the details
|
||||
QScriptValue BaseScriptEngine::lintScript(const QString& sourceCode, const QString& fileName, const int lineNumber) {
|
||||
if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
|
||||
return unboundNullValue();
|
||||
}
|
||||
const auto syntaxCheck = checkSyntax(sourceCode);
|
||||
if (syntaxCheck.state() != QScriptSyntaxCheckResult::Valid) {
|
||||
auto err = globalObject().property("SyntaxError")
|
||||
|
@ -82,13 +95,16 @@ QScriptValue BaseScriptEngine::lintScript(const QString& sourceCode, const QStri
|
|||
}
|
||||
return err;
|
||||
}
|
||||
return undefinedValue();
|
||||
return QScriptValue();
|
||||
}
|
||||
|
||||
// this pulls from the best available information to create a detailed snapshot of the current exception
|
||||
QScriptValue BaseScriptEngine::cloneUncaughtException(const QString& extraDetail) {
|
||||
if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
|
||||
return unboundNullValue();
|
||||
}
|
||||
if (!hasUncaughtException()) {
|
||||
return QScriptValue();
|
||||
return unboundNullValue();
|
||||
}
|
||||
auto exception = uncaughtException();
|
||||
// ensure the error object is engine-local
|
||||
|
@ -144,7 +160,10 @@ QScriptValue BaseScriptEngine::cloneUncaughtException(const QString& extraDetail
|
|||
return err;
|
||||
}
|
||||
|
||||
QString BaseScriptEngine::formatException(const QScriptValue& exception) {
|
||||
QString BaseScriptEngine::formatException(const QScriptValue& exception, bool includeExtendedDetails) {
|
||||
if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
|
||||
return QString();
|
||||
}
|
||||
QString note { "UncaughtException" };
|
||||
QString result;
|
||||
|
||||
|
@ -156,8 +175,8 @@ QString BaseScriptEngine::formatException(const QScriptValue& exception) {
|
|||
const auto lineNumber = exception.property("lineNumber").toString();
|
||||
const auto stacktrace = exception.property("stack").toString();
|
||||
|
||||
if (_enableExtendedJSExceptions.get()) {
|
||||
// This setting toggles display of the hints now being added during the loading process.
|
||||
if (includeExtendedDetails) {
|
||||
// Display additional exception / troubleshooting hints that can be added via the custom Error .detail property
|
||||
// Example difference:
|
||||
// [UncaughtExceptions] Error: Can't find variable: foobar in atp:/myentity.js\n...
|
||||
// [UncaughtException (construct {1eb5d3fa-23b1-411c-af83-163af7220e3f})] Error: Can't find variable: foobar in atp:/myentity.js\n...
|
||||
|
@ -173,14 +192,39 @@ QString BaseScriptEngine::formatException(const QScriptValue& exception) {
|
|||
return result;
|
||||
}
|
||||
|
||||
bool BaseScriptEngine::raiseException(const QScriptValue& exception) {
|
||||
if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
|
||||
return false;
|
||||
}
|
||||
if (currentContext()) {
|
||||
// we have an active context / JS stack frame so throw the exception per usual
|
||||
currentContext()->throwValue(makeError(exception));
|
||||
return true;
|
||||
} else {
|
||||
// we are within a pure C++ stack frame (ie: being called directly by other C++ code)
|
||||
// in this case no context information is available so just emit the exception for reporting
|
||||
emit unhandledException(makeError(exception));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool BaseScriptEngine::maybeEmitUncaughtException(const QString& debugHint) {
|
||||
if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
|
||||
return false;
|
||||
}
|
||||
if (!isEvaluating() && hasUncaughtException()) {
|
||||
emit unhandledException(cloneUncaughtException(debugHint));
|
||||
clearExceptions();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
QScriptValue BaseScriptEngine::evaluateInClosure(const QScriptValue& closure, const QScriptProgram& program) {
|
||||
PROFILE_RANGE(script, "evaluateInClosure");
|
||||
if (QThread::currentThread() != thread()) {
|
||||
qCCritical(scriptengine) << "*** CRITICAL *** ScriptEngine::evaluateInClosure() is meant to be called from engine thread only.";
|
||||
// note: a recursive mutex might be needed around below code if this method ever becomes Q_INVOKABLE
|
||||
return QScriptValue();
|
||||
if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
|
||||
return unboundNullValue();
|
||||
}
|
||||
|
||||
const auto fileName = program.fileName();
|
||||
const auto shortName = QUrl(fileName).fileName();
|
||||
|
||||
|
@ -189,7 +233,7 @@ QScriptValue BaseScriptEngine::evaluateInClosure(const QScriptValue& closure, co
|
|||
auto global = closure.property("global");
|
||||
if (global.isObject()) {
|
||||
#ifdef DEBUG_JS
|
||||
qCDebug(scriptengine) << " setting global = closure.global" << shortName;
|
||||
qCDebug(shared) << " setting global = closure.global" << shortName;
|
||||
#endif
|
||||
oldGlobal = globalObject();
|
||||
setGlobalObject(global);
|
||||
|
@ -200,34 +244,34 @@ QScriptValue BaseScriptEngine::evaluateInClosure(const QScriptValue& closure, co
|
|||
auto thiz = closure.property("this");
|
||||
if (thiz.isObject()) {
|
||||
#ifdef DEBUG_JS
|
||||
qCDebug(scriptengine) << " setting this = closure.this" << shortName;
|
||||
qCDebug(shared) << " setting this = closure.this" << shortName;
|
||||
#endif
|
||||
context->setThisObject(thiz);
|
||||
}
|
||||
|
||||
context->pushScope(closure);
|
||||
#ifdef DEBUG_JS
|
||||
qCDebug(scriptengine) << QString("[%1] evaluateInClosure %2").arg(isEvaluating()).arg(shortName);
|
||||
qCDebug(shared) << QString("[%1] evaluateInClosure %2").arg(isEvaluating()).arg(shortName);
|
||||
#endif
|
||||
{
|
||||
result = BaseScriptEngine::evaluate(program);
|
||||
if (hasUncaughtException()) {
|
||||
auto err = cloneUncaughtException(__FUNCTION__);
|
||||
#ifdef DEBUG_JS_EXCEPTIONS
|
||||
qCWarning(scriptengine) << __FUNCTION__ << "---------- hasCaught:" << err.toString() << result.toString();
|
||||
qCWarning(shared) << __FUNCTION__ << "---------- hasCaught:" << err.toString() << result.toString();
|
||||
err.setProperty("_result", result);
|
||||
#endif
|
||||
result = err;
|
||||
}
|
||||
}
|
||||
#ifdef DEBUG_JS
|
||||
qCDebug(scriptengine) << QString("[%1] //evaluateInClosure %2").arg(isEvaluating()).arg(shortName);
|
||||
qCDebug(shared) << QString("[%1] //evaluateInClosure %2").arg(isEvaluating()).arg(shortName);
|
||||
#endif
|
||||
popContext();
|
||||
|
||||
if (oldGlobal.isValid()) {
|
||||
#ifdef DEBUG_JS
|
||||
qCDebug(scriptengine) << " restoring global" << shortName;
|
||||
qCDebug(shared) << " restoring global" << shortName;
|
||||
#endif
|
||||
setGlobalObject(oldGlobal);
|
||||
}
|
||||
|
@ -236,7 +280,6 @@ QScriptValue BaseScriptEngine::evaluateInClosure(const QScriptValue& closure, co
|
|||
}
|
||||
|
||||
// Lambda
|
||||
|
||||
QScriptValue BaseScriptEngine::newLambdaFunction(std::function<QScriptValue(QScriptContext *, QScriptEngine*)> operation, const QScriptValue& data, const QScriptEngine::ValueOwnership& ownership) {
|
||||
auto lambda = new Lambda(this, operation, data);
|
||||
auto object = newQObject(lambda, ownership);
|
||||
|
@ -262,26 +305,57 @@ Lambda::Lambda(QScriptEngine *engine, std::function<QScriptValue(QScriptContext
|
|||
#endif
|
||||
}
|
||||
QScriptValue Lambda::call() {
|
||||
if (!BaseScriptEngine::IS_THREADSAFE_INVOCATION(engine->thread(), __FUNCTION__)) {
|
||||
return BaseScriptEngine::unboundNullValue();
|
||||
}
|
||||
return operation(engine->currentContext(), engine);
|
||||
}
|
||||
|
||||
QScriptValue makeScopedHandlerObject(QScriptValue scopeOrCallback, QScriptValue methodOrName) {
|
||||
auto engine = scopeOrCallback.engine();
|
||||
if (!engine) {
|
||||
return scopeOrCallback;
|
||||
}
|
||||
auto scope = QScriptValue();
|
||||
auto callback = scopeOrCallback;
|
||||
if (scopeOrCallback.isObject()) {
|
||||
if (methodOrName.isString()) {
|
||||
scope = scopeOrCallback;
|
||||
callback = scope.property(methodOrName.toString());
|
||||
} else if (methodOrName.isFunction()) {
|
||||
scope = scopeOrCallback;
|
||||
callback = methodOrName;
|
||||
}
|
||||
}
|
||||
auto handler = engine->newObject();
|
||||
handler.setProperty("scope", scope);
|
||||
handler.setProperty("callback", callback);
|
||||
return handler;
|
||||
}
|
||||
|
||||
QScriptValue callScopedHandlerObject(QScriptValue handler, QScriptValue err, QScriptValue result) {
|
||||
return handler.property("callback").call(handler.property("scope"), QScriptValueList({ err, result }));
|
||||
}
|
||||
|
||||
#ifdef DEBUG_JS
|
||||
void BaseScriptEngine::_debugDump(const QString& header, const QScriptValue& object, const QString& footer) {
|
||||
if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
|
||||
return;
|
||||
}
|
||||
if (!header.isEmpty()) {
|
||||
qCDebug(scriptengine) << header;
|
||||
qCDebug(shared) << header;
|
||||
}
|
||||
if (!object.isObject()) {
|
||||
qCDebug(scriptengine) << "(!isObject)" << object.toVariant().toString() << object.toString();
|
||||
qCDebug(shared) << "(!isObject)" << object.toVariant().toString() << object.toString();
|
||||
return;
|
||||
}
|
||||
QScriptValueIterator it(object);
|
||||
while (it.hasNext()) {
|
||||
it.next();
|
||||
qCDebug(scriptengine) << it.name() << ":" << it.value().toString();
|
||||
qCDebug(shared) << it.name() << ":" << it.value().toString();
|
||||
}
|
||||
if (!footer.isEmpty()) {
|
||||
qCDebug(scriptengine) << footer;
|
||||
qCDebug(shared) << footer;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
90
libraries/shared/src/BaseScriptEngine.h
Normal file
90
libraries/shared/src/BaseScriptEngine.h
Normal file
|
@ -0,0 +1,90 @@
|
|||
//
|
||||
// BaseScriptEngine.h
|
||||
// libraries/script-engine/src
|
||||
//
|
||||
// Created by Timothy Dedischew on 02/01/17.
|
||||
// Copyright 2017 High Fidelity, Inc.
|
||||
//
|
||||
// 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_BaseScriptEngine_h
|
||||
#define hifi_BaseScriptEngine_h
|
||||
|
||||
#include <functional>
|
||||
#include <QtCore/QDebug>
|
||||
#include <QtScript/QScriptEngine>
|
||||
|
||||
// common base class for extending QScriptEngine itself
|
||||
class BaseScriptEngine : public QScriptEngine, public QEnableSharedFromThis<BaseScriptEngine> {
|
||||
Q_OBJECT
|
||||
public:
|
||||
static const QString SCRIPT_EXCEPTION_FORMAT;
|
||||
static const QString SCRIPT_BACKTRACE_SEP;
|
||||
|
||||
// threadsafe "unbound" version of QScriptEngine::nullValue()
|
||||
static const QScriptValue unboundNullValue() { return QScriptValue(0, QScriptValue::NullValue); }
|
||||
|
||||
BaseScriptEngine() {}
|
||||
|
||||
Q_INVOKABLE QScriptValue lintScript(const QString& sourceCode, const QString& fileName, const int lineNumber = 1);
|
||||
Q_INVOKABLE QScriptValue makeError(const QScriptValue& other = QScriptValue(), const QString& type = "Error");
|
||||
Q_INVOKABLE QString formatException(const QScriptValue& exception, bool includeExtendedDetails);
|
||||
|
||||
QScriptValue cloneUncaughtException(const QString& detail = QString());
|
||||
QScriptValue evaluateInClosure(const QScriptValue& locals, const QScriptProgram& program);
|
||||
|
||||
// if there is a pending exception and we are at the top level (non-recursive) stack frame, this emits and resets it
|
||||
bool maybeEmitUncaughtException(const QString& debugHint = QString());
|
||||
|
||||
// if the currentContext() is valid then throw the passed exception; otherwise, immediately emit it.
|
||||
// note: this is used in cases where C++ code might call into JS API methods directly
|
||||
bool raiseException(const QScriptValue& exception);
|
||||
|
||||
// helper to detect and log warnings when other code invokes QScriptEngine/BaseScriptEngine in thread-unsafe ways
|
||||
static bool IS_THREADSAFE_INVOCATION(const QThread *thread, const QString& method);
|
||||
signals:
|
||||
void unhandledException(const QScriptValue& exception);
|
||||
|
||||
protected:
|
||||
// like `newFunction`, but allows mapping inline C++ lambdas with captures as callable QScriptValues
|
||||
// even though the context/engine parameters are redundant in most cases, the function signature matches `newFunction`
|
||||
// anyway so that newLambdaFunction can be used to rapidly prototype / test utility APIs and then if becoming
|
||||
// permanent more easily promoted into regular static newFunction scenarios.
|
||||
QScriptValue newLambdaFunction(std::function<QScriptValue(QScriptContext *context, QScriptEngine* engine)> operation, const QScriptValue& data = QScriptValue(), const QScriptEngine::ValueOwnership& ownership = QScriptEngine::AutoOwnership);
|
||||
|
||||
#ifdef DEBUG_JS
|
||||
static void _debugDump(const QString& header, const QScriptValue& object, const QString& footer = QString());
|
||||
#endif
|
||||
};
|
||||
|
||||
// Standardized CPS callback helpers (see: http://fredkschott.com/post/2014/03/understanding-error-first-callbacks-in-node-js/)
|
||||
// These two helpers allow async JS APIs that use a callback parameter to be more friendly to scripters by accepting thisObject
|
||||
// context and adopting a consistent and intuitable callback signature:
|
||||
// function callback(err, result) { if (err) { ... } else { /* do stuff with result */ } }
|
||||
//
|
||||
// To use, first pass the user-specified callback args in the same order used with optionally-scoped Qt signal connections:
|
||||
// auto handler = makeScopedHandlerObject(scopeOrCallback, optionalMethodOrName);
|
||||
// And then invoke the scoped handler later per CPS conventions:
|
||||
// auto result = callScopedHandlerObject(handler, err, result);
|
||||
QScriptValue makeScopedHandlerObject(QScriptValue scopeOrCallback, QScriptValue methodOrName);
|
||||
QScriptValue callScopedHandlerObject(QScriptValue handler, QScriptValue err, QScriptValue result);
|
||||
|
||||
// Lambda helps create callable QScriptValues out of std::functions:
|
||||
// (just meant for use from within the script engine itself)
|
||||
class Lambda : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
Lambda(QScriptEngine *engine, std::function<QScriptValue(QScriptContext *context, QScriptEngine* engine)> operation, QScriptValue data);
|
||||
~Lambda();
|
||||
public slots:
|
||||
QScriptValue call();
|
||||
QString toString() const;
|
||||
private:
|
||||
QScriptEngine* engine;
|
||||
std::function<QScriptValue(QScriptContext *context, QScriptEngine* engine)> operation;
|
||||
QScriptValue data;
|
||||
};
|
||||
|
||||
#endif // hifi_BaseScriptEngine_h
|
|
@ -6,7 +6,7 @@
|
|||
var lastSpecStartTime;
|
||||
function ConsoleReporter(options) {
|
||||
var startTime = new Date().getTime();
|
||||
var errorCount = 0;
|
||||
var errorCount = 0, pending = [];
|
||||
this.jasmineStarted = function (obj) {
|
||||
print('Jasmine started with ' + obj.totalSpecsDefined + ' tests.');
|
||||
};
|
||||
|
@ -15,11 +15,14 @@
|
|||
var endTime = new Date().getTime();
|
||||
print('<hr />');
|
||||
if (errorCount === 0) {
|
||||
print ('<span style="color:green">All tests passed!</span>');
|
||||
print ('<span style="color:green">All enabled tests passed!</span>');
|
||||
} else {
|
||||
print('<span style="color:red">Tests completed with ' +
|
||||
errorCount + ' ' + ERROR + '.<span>');
|
||||
}
|
||||
if (pending.length)
|
||||
print ('<span style="color:darkorange">disabled: <br /> '+
|
||||
pending.join('<br /> ')+'</span>');
|
||||
print('Tests completed in ' + (endTime - startTime) + 'ms.');
|
||||
};
|
||||
this.suiteStarted = function(obj) {
|
||||
|
@ -32,6 +35,10 @@
|
|||
lastSpecStartTime = new Date().getTime();
|
||||
};
|
||||
this.specDone = function(obj) {
|
||||
if (obj.status === 'pending') {
|
||||
pending.push(obj.fullName);
|
||||
return print('...(pending ' + obj.fullName +')');
|
||||
}
|
||||
var specEndTime = new Date().getTime();
|
||||
var symbol = obj.status === PASSED ?
|
||||
'<span style="color:green">' + CHECKMARK + '</span>' :
|
||||
|
@ -55,7 +62,7 @@
|
|||
clearTimeout = Script.clearTimeout;
|
||||
clearInterval = Script.clearInterval;
|
||||
|
||||
var jasmine = jasmineRequire.core(jasmineRequire);
|
||||
var jasmine = this.jasmine = jasmineRequire.core(jasmineRequire);
|
||||
|
||||
var env = jasmine.getEnv();
|
||||
|
||||
|
|
10
scripts/developer/tests/unit_tests/moduleTests/cycles/a.js
Normal file
10
scripts/developer/tests/unit_tests/moduleTests/cycles/a.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
/* eslint-env node */
|
||||
var a = exports;
|
||||
a.done = false;
|
||||
var b = require('./b.js');
|
||||
a.done = true;
|
||||
a.name = 'a';
|
||||
a['a.done?'] = a.done;
|
||||
a['b.done?'] = b.done;
|
||||
|
||||
print('from a.js a.done =', a.done, '/ b.done =', b.done);
|
10
scripts/developer/tests/unit_tests/moduleTests/cycles/b.js
Normal file
10
scripts/developer/tests/unit_tests/moduleTests/cycles/b.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
/* eslint-env node */
|
||||
var b = exports;
|
||||
b.done = false;
|
||||
var a = require('./a.js');
|
||||
b.done = true;
|
||||
b.name = 'b';
|
||||
b['a.done?'] = a.done;
|
||||
b['b.done?'] = b.done;
|
||||
|
||||
print('from b.js a.done =', a.done, '/ b.done =', b.done);
|
|
@ -0,0 +1,17 @@
|
|||
/* eslint-env node */
|
||||
/* global print */
|
||||
/* eslint-disable comma-dangle */
|
||||
|
||||
print('main.js');
|
||||
var a = require('./a.js'),
|
||||
b = require('./b.js');
|
||||
|
||||
print('from main.js a.done =', a.done, 'and b.done =', b.done);
|
||||
|
||||
module.exports = {
|
||||
name: 'main',
|
||||
a: a,
|
||||
b: b,
|
||||
'a.done?': a.done,
|
||||
'b.done?': b.done,
|
||||
};
|
|
@ -0,0 +1,13 @@
|
|||
/* eslint-disable comma-dangle */
|
||||
// test module method exception being thrown within main constructor
|
||||
(function() {
|
||||
var apiMethod = Script.require('../exceptions/exceptionInFunction.js');
|
||||
print(Script.resolvePath(''), "apiMethod", apiMethod);
|
||||
// this next line throws from within apiMethod
|
||||
print(apiMethod());
|
||||
return {
|
||||
preload: function(uuid) {
|
||||
print("entityConstructorAPIException::preload -- never seen --", uuid, Script.resolvePath(''));
|
||||
},
|
||||
};
|
||||
});
|
|
@ -0,0 +1,23 @@
|
|||
/* global module */
|
||||
/* eslint-disable comma-dangle */
|
||||
// test dual-purpose module and standalone Entity script
|
||||
function MyEntity(filename) {
|
||||
return {
|
||||
preload: function(uuid) {
|
||||
print("entityConstructorModule.js::preload");
|
||||
if (typeof module === 'object') {
|
||||
print("module.filename", module.filename);
|
||||
print("module.parent.filename", module.parent && module.parent.filename);
|
||||
}
|
||||
},
|
||||
clickDownOnEntity: function(uuid, evt) {
|
||||
print("entityConstructorModule.js::clickDownOnEntity");
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
module.exports = MyEntity;
|
||||
} catch (e) {} // eslint-disable-line no-empty
|
||||
print('entityConstructorModule::MyEntity', typeof MyEntity);
|
||||
(MyEntity);
|
|
@ -0,0 +1,14 @@
|
|||
/* global module */
|
||||
// test Entity constructor based on inherited constructor from a module
|
||||
function constructor() {
|
||||
print("entityConstructorNested::constructor");
|
||||
var MyEntity = Script.require('./entityConstructorModule.js');
|
||||
return new MyEntity("-- created from entityConstructorNested --");
|
||||
}
|
||||
|
||||
try {
|
||||
module.exports = constructor;
|
||||
} catch (e) {
|
||||
constructor;
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
/* global module */
|
||||
// test Entity constructor based on nested, inherited module constructors
|
||||
function constructor() {
|
||||
print("entityConstructorNested2::constructor");
|
||||
|
||||
// inherit from entityConstructorNested
|
||||
var MyEntity = Script.require('./entityConstructorNested.js');
|
||||
function SubEntity() {}
|
||||
SubEntity.prototype = new MyEntity('-- created from entityConstructorNested2 --');
|
||||
|
||||
// create new instance
|
||||
var entity = new SubEntity();
|
||||
// "override" clickDownOnEntity for just this new instance
|
||||
entity.clickDownOnEntity = function(uuid, evt) {
|
||||
print("entityConstructorNested2::clickDownOnEntity");
|
||||
SubEntity.prototype.clickDownOnEntity.apply(this, arguments);
|
||||
};
|
||||
return entity;
|
||||
}
|
||||
|
||||
try {
|
||||
module.exports = constructor;
|
||||
} catch (e) {
|
||||
constructor;
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
/* eslint-disable comma-dangle */
|
||||
// test module-related exception from within "require" evaluation itself
|
||||
(function() {
|
||||
var mod = Script.require('../exceptions/exception.js');
|
||||
return {
|
||||
preload: function(uuid) {
|
||||
print("entityConstructorRequireException::preload (never happens)", uuid, Script.resolvePath(''), mod);
|
||||
},
|
||||
};
|
||||
});
|
|
@ -0,0 +1,13 @@
|
|||
/* eslint-disable comma-dangle */
|
||||
// test module method exception being thrown within preload
|
||||
(function() {
|
||||
var apiMethod = Script.require('../exceptions/exceptionInFunction.js');
|
||||
print(Script.resolvePath(''), "apiMethod", apiMethod);
|
||||
return {
|
||||
preload: function(uuid) {
|
||||
// this next line throws from within apiMethod
|
||||
print(apiMethod());
|
||||
print("entityPreloadAPIException::preload -- never seen --", uuid, Script.resolvePath(''));
|
||||
},
|
||||
};
|
||||
});
|
|
@ -0,0 +1,11 @@
|
|||
/* eslint-disable comma-dangle */
|
||||
// test requiring a module from within preload
|
||||
(function constructor() {
|
||||
return {
|
||||
preload: function(uuid) {
|
||||
print("entityPreloadRequire::preload");
|
||||
var example = Script.require('../example.json');
|
||||
print("entityPreloadRequire::example::name", example.name);
|
||||
},
|
||||
};
|
||||
});
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"name": "Example JSON Module",
|
||||
"last-modified": 1485789862,
|
||||
"config": {
|
||||
"title": "My Title",
|
||||
"width": 800,
|
||||
"height": 600
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
/* eslint-env node */
|
||||
module.exports = "n/a";
|
||||
throw new Error('exception on line 2');
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
/* eslint-env node */
|
||||
// dummy lines to make sure exception line number is well below parent test script
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
|
||||
|
||||
function myfunc() {
|
||||
throw new Error('exception on line 32 in myfunc');
|
||||
}
|
||||
module.exports = myfunc;
|
||||
if (Script[module.filename] === 'throw') {
|
||||
myfunc();
|
||||
}
|
378
scripts/developer/tests/unit_tests/moduleUnitTests.js
Normal file
378
scripts/developer/tests/unit_tests/moduleUnitTests.js
Normal file
|
@ -0,0 +1,378 @@
|
|||
/* eslint-env jasmine, node */
|
||||
/* global print:true, Script:true, global:true, require:true */
|
||||
/* eslint-disable comma-dangle */
|
||||
var isNode = instrumentTestrunner(),
|
||||
runInterfaceTests = !isNode,
|
||||
runNetworkTests = true;
|
||||
|
||||
// describe wrappers (note: `xdescribe` indicates a disabled or "pending" jasmine test)
|
||||
var INTERFACE = { describe: runInterfaceTests ? describe : xdescribe },
|
||||
NETWORK = { describe: runNetworkTests ? describe : xdescribe };
|
||||
|
||||
describe('require', function() {
|
||||
describe('resolve', function() {
|
||||
it('should resolve relative filenames', function() {
|
||||
var expected = Script.resolvePath('./moduleTests/example.json');
|
||||
expect(require.resolve('./moduleTests/example.json')).toEqual(expected);
|
||||
});
|
||||
describe('exceptions', function() {
|
||||
it('should reject blank "" module identifiers', function() {
|
||||
expect(function() {
|
||||
require.resolve('');
|
||||
}).toThrowError(/Cannot find/);
|
||||
});
|
||||
it('should reject excessive identifier sizes', function() {
|
||||
expect(function() {
|
||||
require.resolve(new Array(8193).toString());
|
||||
}).toThrowError(/Cannot find/);
|
||||
});
|
||||
it('should reject implicitly-relative filenames', function() {
|
||||
expect(function() {
|
||||
var mod = require.resolve('example.js');
|
||||
mod.exists;
|
||||
}).toThrowError(/Cannot find/);
|
||||
});
|
||||
it('should reject unanchored, existing filenames with advice', function() {
|
||||
expect(function() {
|
||||
var mod = require.resolve('moduleTests/example.json');
|
||||
mod.exists;
|
||||
}).toThrowError(/use '.\/moduleTests\/example\.json'/);
|
||||
});
|
||||
it('should reject unanchored, non-existing filenames', function() {
|
||||
expect(function() {
|
||||
var mod = require.resolve('asdfssdf/example.json');
|
||||
mod.exists;
|
||||
}).toThrowError(/Cannot find.*system module not found/);
|
||||
});
|
||||
it('should reject non-existent filenames', function() {
|
||||
expect(function() {
|
||||
require.resolve('./404error.js');
|
||||
}).toThrowError(/Cannot find/);
|
||||
});
|
||||
it('should reject identifiers resolving to a directory', function() {
|
||||
expect(function() {
|
||||
var mod = require.resolve('.');
|
||||
mod.exists;
|
||||
// console.warn('resolved(.)', mod);
|
||||
}).toThrowError(/Cannot find/);
|
||||
expect(function() {
|
||||
var mod = require.resolve('..');
|
||||
mod.exists;
|
||||
// console.warn('resolved(..)', mod);
|
||||
}).toThrowError(/Cannot find/);
|
||||
expect(function() {
|
||||
var mod = require.resolve('../');
|
||||
mod.exists;
|
||||
// console.warn('resolved(../)', mod);
|
||||
}).toThrowError(/Cannot find/);
|
||||
});
|
||||
(isNode ? xit : it)('should reject non-system, extensionless identifiers', function() {
|
||||
expect(function() {
|
||||
require.resolve('./example');
|
||||
}).toThrowError(/Cannot find/);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('JSON', function() {
|
||||
it('should import .json modules', function() {
|
||||
var example = require('./moduleTests/example.json');
|
||||
expect(example.name).toEqual('Example JSON Module');
|
||||
});
|
||||
// noet: support for loading JSON via content type workarounds reverted
|
||||
// (leaving these tests intact in case ever revisited later)
|
||||
// INTERFACE.describe('interface', function() {
|
||||
// NETWORK.describe('network', function() {
|
||||
// xit('should import #content-type=application/json modules', function() {
|
||||
// var results = require('https://jsonip.com#content-type=application/json');
|
||||
// expect(results.ip).toMatch(/^[.0-9]+$/);
|
||||
// });
|
||||
// xit('should import content-type: application/json modules', function() {
|
||||
// var scope = { 'content-type': 'application/json' };
|
||||
// var results = require.call(scope, 'https://jsonip.com');
|
||||
// expect(results.ip).toMatch(/^[.0-9]+$/);
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
|
||||
});
|
||||
|
||||
INTERFACE.describe('system', function() {
|
||||
it('require("vec3")', function() {
|
||||
expect(require('vec3')).toEqual(jasmine.any(Function));
|
||||
});
|
||||
it('require("vec3").method', function() {
|
||||
expect(require('vec3')().isValid).toEqual(jasmine.any(Function));
|
||||
});
|
||||
it('require("vec3") as constructor', function() {
|
||||
var vec3 = require('vec3');
|
||||
var v = vec3(1.1, 2.2, 3.3);
|
||||
expect(v).toEqual(jasmine.any(Object));
|
||||
expect(v.isValid).toEqual(jasmine.any(Function));
|
||||
expect(v.isValid()).toBe(true);
|
||||
expect(v.toString()).toEqual('[Vec3 (1.100,2.200,3.300)]');
|
||||
});
|
||||
});
|
||||
|
||||
describe('cache', function() {
|
||||
it('should cache modules by resolved module id', function() {
|
||||
var value = new Date;
|
||||
var example = require('./moduleTests/example.json');
|
||||
// earmark the module object with a unique value
|
||||
example['.test'] = value;
|
||||
var example2 = require('../../tests/unit_tests/moduleTests/example.json');
|
||||
expect(example2).toBe(example);
|
||||
// verify earmark is still the same after a second require()
|
||||
expect(example2['.test']).toBe(example['.test']);
|
||||
});
|
||||
it('should reload cached modules set to null', function() {
|
||||
var value = new Date;
|
||||
var example = require('./moduleTests/example.json');
|
||||
example['.test'] = value;
|
||||
require.cache[require.resolve('../../tests/unit_tests/moduleTests/example.json')] = null;
|
||||
var example2 = require('../../tests/unit_tests/moduleTests/example.json');
|
||||
// verify the earmark is *not* the same as before
|
||||
expect(example2).not.toBe(example);
|
||||
expect(example2['.test']).not.toBe(example['.test']);
|
||||
});
|
||||
it('should reload when module property is deleted', function() {
|
||||
var value = new Date;
|
||||
var example = require('./moduleTests/example.json');
|
||||
example['.test'] = value;
|
||||
delete require.cache[require.resolve('../../tests/unit_tests/moduleTests/example.json')];
|
||||
var example2 = require('../../tests/unit_tests/moduleTests/example.json');
|
||||
// verify the earmark is *not* the same as before
|
||||
expect(example2).not.toBe(example);
|
||||
expect(example2['.test']).not.toBe(example['.test']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('cyclic dependencies', function() {
|
||||
describe('should allow lazy-ref cyclic module resolution', function() {
|
||||
var main;
|
||||
beforeEach(function() {
|
||||
// eslint-disable-next-line
|
||||
try { this._print = print; } catch (e) {}
|
||||
// during these tests print() is no-op'd so that it doesn't disrupt the reporter output
|
||||
print = function() {};
|
||||
Script.resetModuleCache();
|
||||
});
|
||||
afterEach(function() {
|
||||
print = this._print;
|
||||
});
|
||||
it('main is requirable', function() {
|
||||
main = require('./moduleTests/cycles/main.js');
|
||||
expect(main).toEqual(jasmine.any(Object));
|
||||
});
|
||||
it('transient a and b done values', function() {
|
||||
expect(main.a['b.done?']).toBe(true);
|
||||
expect(main.b['a.done?']).toBe(false);
|
||||
});
|
||||
it('ultimate a.done?', function() {
|
||||
expect(main['a.done?']).toBe(true);
|
||||
});
|
||||
it('ultimate b.done?', function() {
|
||||
expect(main['b.done?']).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('JS', function() {
|
||||
it('should throw catchable local file errors', function() {
|
||||
expect(function() {
|
||||
require('file:///dev/null/non-existent-file.js');
|
||||
}).toThrowError(/path not found|Cannot find.*non-existent-file/);
|
||||
});
|
||||
it('should throw catchable invalid id errors', function() {
|
||||
expect(function() {
|
||||
require(new Array(4096 * 2).toString());
|
||||
}).toThrowError(/invalid.*size|Cannot find.*,{30}/);
|
||||
});
|
||||
it('should throw catchable unresolved id errors', function() {
|
||||
expect(function() {
|
||||
require('foobar:/baz.js');
|
||||
}).toThrowError(/could not resolve|Cannot find.*foobar:/);
|
||||
});
|
||||
|
||||
NETWORK.describe('network', function() {
|
||||
// note: depending on retries these tests can take up to 60 seconds each to timeout
|
||||
var timeout = 75 * 1000;
|
||||
it('should throw catchable host errors', function() {
|
||||
expect(function() {
|
||||
var mod = require('http://non.existent.highfidelity.io/moduleUnitTest.js');
|
||||
print("mod", Object.keys(mod));
|
||||
}).toThrowError(/error retrieving script .ServerUnavailable.|Cannot find.*non.existent/);
|
||||
}, timeout);
|
||||
it('should throw catchable network timeouts', function() {
|
||||
expect(function() {
|
||||
require('http://ping.highfidelity.io:1024');
|
||||
}).toThrowError(/error retrieving script .Timeout.|Cannot find.*ping.highfidelity/);
|
||||
}, timeout);
|
||||
});
|
||||
});
|
||||
|
||||
INTERFACE.describe('entity', function() {
|
||||
var sampleScripts = [
|
||||
'entityConstructorAPIException.js',
|
||||
'entityConstructorModule.js',
|
||||
'entityConstructorNested2.js',
|
||||
'entityConstructorNested.js',
|
||||
'entityConstructorRequireException.js',
|
||||
'entityPreloadAPIError.js',
|
||||
'entityPreloadRequire.js',
|
||||
].filter(Boolean).map(function(id) {
|
||||
return Script.require.resolve('./moduleTests/entity/'+id);
|
||||
});
|
||||
|
||||
var uuids = [];
|
||||
function cleanup() {
|
||||
uuids.splice(0,uuids.length).forEach(function(uuid) {
|
||||
Entities.deleteEntity(uuid);
|
||||
});
|
||||
}
|
||||
afterAll(cleanup);
|
||||
// extra sanity check to avoid lingering entities
|
||||
Script.scriptEnding.connect(cleanup);
|
||||
|
||||
for (var i=0; i < sampleScripts.length; i++) {
|
||||
maketest(i);
|
||||
}
|
||||
|
||||
function maketest(i) {
|
||||
var script = sampleScripts[ i % sampleScripts.length ];
|
||||
var shortname = '['+i+'] ' + script.split('/').pop();
|
||||
var position = MyAvatar.position;
|
||||
position.y -= i/2;
|
||||
// define a unique jasmine test for the current entity script
|
||||
it(shortname, function(done) {
|
||||
var uuid = Entities.addEntity({
|
||||
text: shortname,
|
||||
description: Script.resolvePath('').split('/').pop(),
|
||||
type: 'Text',
|
||||
position: position,
|
||||
rotation: MyAvatar.orientation,
|
||||
script: script,
|
||||
scriptTimestamp: +new Date,
|
||||
lifetime: 20,
|
||||
lineHeight: 1/8,
|
||||
dimensions: { x: 2, y: 0.5, z: 0.01 },
|
||||
backgroundColor: { red: 0, green: 0, blue: 0 },
|
||||
color: { red: 0xff, green: 0xff, blue: 0xff },
|
||||
}, !Entities.serversExist() || !Entities.canRezTmp());
|
||||
uuids.push(uuid);
|
||||
function stopChecking() {
|
||||
if (ii) {
|
||||
Script.clearInterval(ii);
|
||||
ii = 0;
|
||||
}
|
||||
}
|
||||
var ii = Script.setInterval(function() {
|
||||
Entities.queryPropertyMetadata(uuid, "script", function(err, result) {
|
||||
if (err) {
|
||||
stopChecking();
|
||||
throw new Error(err);
|
||||
}
|
||||
if (result.success) {
|
||||
stopChecking();
|
||||
if (/Exception/.test(script)) {
|
||||
expect(result.status).toMatch(/^error_(loading|running)_script$/);
|
||||
} else {
|
||||
expect(result.status).toEqual("running");
|
||||
}
|
||||
Entities.deleteEntity(uuid);
|
||||
done();
|
||||
} else {
|
||||
print('!result.success', JSON.stringify(result));
|
||||
}
|
||||
});
|
||||
}, 100);
|
||||
Script.setTimeout(stopChecking, 4900);
|
||||
}, 5000 /* jasmine async timeout */);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// support for isomorphic Node.js / Interface unit testing
|
||||
// note: run `npm install` from unit_tests/ and then `node moduleUnitTests.js`
|
||||
function run() {}
|
||||
function instrumentTestrunner() {
|
||||
var isNode = typeof process === 'object' && process.title === 'node';
|
||||
if (typeof describe === 'function') {
|
||||
// already running within a test runner; assume jasmine is ready-to-go
|
||||
return isNode;
|
||||
}
|
||||
if (isNode) {
|
||||
/* eslint-disable no-console */
|
||||
// Node.js test mode
|
||||
// to keep things consistent Node.js uses the local jasmine.js library (instead of an npm version)
|
||||
var jasmineRequire = require('../../libraries/jasmine/jasmine.js');
|
||||
var jasmine = jasmineRequire.core(jasmineRequire);
|
||||
var env = jasmine.getEnv();
|
||||
var jasmineInterface = jasmineRequire.interface(jasmine, env);
|
||||
for (var p in jasmineInterface) {
|
||||
global[p] = jasmineInterface[p];
|
||||
}
|
||||
env.addReporter(new (require('jasmine-console-reporter')));
|
||||
// testing mocks
|
||||
Script = {
|
||||
resetModuleCache: function() {
|
||||
module.require.cache = {};
|
||||
},
|
||||
setTimeout: setTimeout,
|
||||
clearTimeout: clearTimeout,
|
||||
resolvePath: function(id) {
|
||||
// this attempts to accurately emulate how Script.resolvePath works
|
||||
var trace = {}; Error.captureStackTrace(trace);
|
||||
var base = trace.stack.split('\n')[2].replace(/^.*[(]|[)].*$/g,'').replace(/:[0-9]+:[0-9]+.*$/,'');
|
||||
if (!id) {
|
||||
return base;
|
||||
}
|
||||
var rel = base.replace(/[^\/]+$/, id);
|
||||
console.info('rel', rel);
|
||||
return require.resolve(rel);
|
||||
},
|
||||
require: function(mod) {
|
||||
return require(Script.require.resolve(mod));
|
||||
},
|
||||
};
|
||||
Script.require.cache = require.cache;
|
||||
Script.require.resolve = function(mod) {
|
||||
if (mod === '.' || /^\.\.($|\/)/.test(mod)) {
|
||||
throw new Error("Cannot find module '"+mod+"' (is dir)");
|
||||
}
|
||||
var path = require.resolve(mod);
|
||||
// console.info('node-require-reoslved', mod, path);
|
||||
try {
|
||||
if (require('fs').lstatSync(path).isDirectory()) {
|
||||
throw new Error("Cannot find module '"+path+"' (is directory)");
|
||||
}
|
||||
// console.info('!path', path);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
return path;
|
||||
};
|
||||
print = console.info.bind(console, '[print]');
|
||||
/* eslint-enable no-console */
|
||||
} else {
|
||||
// Interface test mode
|
||||
global = this;
|
||||
Script.require('../../../system/libraries/utils.js');
|
||||
this.jasmineRequire = Script.require('../../libraries/jasmine/jasmine.js');
|
||||
Script.require('../../libraries/jasmine/hifi-boot.js');
|
||||
require = Script.require;
|
||||
// polyfill console
|
||||
/* global console:true */
|
||||
console = {
|
||||
log: print,
|
||||
info: print.bind(this, '[info]'),
|
||||
warn: print.bind(this, '[warn]'),
|
||||
error: print.bind(this, '[error]'),
|
||||
debug: print.bind(this, '[debug]'),
|
||||
};
|
||||
}
|
||||
// eslint-disable-next-line
|
||||
run = function() { global.jasmine.getEnv().execute(); };
|
||||
return isNode;
|
||||
}
|
||||
run();
|
6
scripts/developer/tests/unit_tests/package.json
Normal file
6
scripts/developer/tests/unit_tests/package.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "unit_tests",
|
||||
"devDependencies": {
|
||||
"jasmine-console-reporter": "^1.2.7"
|
||||
}
|
||||
}
|
|
@ -15,10 +15,20 @@ describe('Script', function () {
|
|||
// characterization tests
|
||||
// initially these are just to capture how the app works currently
|
||||
var testCases = {
|
||||
// special relative resolves
|
||||
'': filename,
|
||||
'.': dirname,
|
||||
'..': parentdir,
|
||||
|
||||
// local file "magic" tilde path expansion
|
||||
'/~/defaultScripts.js': ScriptDiscoveryService.defaultScriptsPath + '/defaultScripts.js',
|
||||
|
||||
// these schemes appear to always get resolved to empty URLs
|
||||
'qrc://test': '',
|
||||
'about:Entities 1': '',
|
||||
'ftp://host:port/path': '',
|
||||
'data:text/html;text,foo': '',
|
||||
|
||||
'Entities 1': dirname + 'Entities 1',
|
||||
'./file.js': dirname + 'file.js',
|
||||
'c:/temp/': 'file:///c:/temp/',
|
||||
|
@ -31,6 +41,12 @@ describe('Script', function () {
|
|||
'/~/libraries/utils.js': 'file:///~/libraries/utils.js',
|
||||
'/temp/file.js': 'file:///temp/file.js',
|
||||
'/~/': 'file:///~/',
|
||||
|
||||
// these schemes appear to always get resolved to the same URL again
|
||||
'http://highfidelity.com': 'http://highfidelity.com',
|
||||
'atp:/highfidelity': 'atp:/highfidelity',
|
||||
'atp:c2d7e3a48cadf9ba75e4f8d9f4d80e75276774880405a093fdee36543aa04f':
|
||||
'atp:c2d7e3a48cadf9ba75e4f8d9f4d80e75276774880405a093fdee36543aa04f',
|
||||
};
|
||||
describe('resolvePath', function () {
|
||||
Object.keys(testCases).forEach(function(input) {
|
||||
|
@ -42,7 +58,7 @@ describe('Script', function () {
|
|||
|
||||
describe('include', function () {
|
||||
var old_cache_buster;
|
||||
var cache_buster = '#' + +new Date;
|
||||
var cache_buster = '#' + new Date().getTime().toString(36);
|
||||
beforeAll(function() {
|
||||
old_cache_buster = Settings.getValue('cache_buster');
|
||||
Settings.setValue('cache_buster', cache_buster);
|
||||
|
|
69
scripts/modules/vec3.js
Normal file
69
scripts/modules/vec3.js
Normal file
|
@ -0,0 +1,69 @@
|
|||
// Example of using a "system module" to decouple Vec3's implementation details.
|
||||
//
|
||||
// Users would bring Vec3 support in as a module:
|
||||
// var vec3 = Script.require('vec3');
|
||||
//
|
||||
|
||||
// (this example is compatible with using as a Script.include and as a Script.require module)
|
||||
try {
|
||||
// Script.require
|
||||
module.exports = vec3;
|
||||
} catch(e) {
|
||||
// Script.include
|
||||
Script.registerValue("vec3", vec3);
|
||||
}
|
||||
|
||||
vec3.fromObject = function(v) {
|
||||
//return new vec3(v.x, v.y, v.z);
|
||||
//... this is even faster and achieves the same effect
|
||||
v.__proto__ = vec3.prototype;
|
||||
return v;
|
||||
};
|
||||
|
||||
vec3.prototype = {
|
||||
multiply: function(v2) {
|
||||
// later on could support overrides like so:
|
||||
// if (v2 instanceof quat) { [...] }
|
||||
// which of the below is faster (C++ or JS)?
|
||||
// (dunno -- but could systematically find out and go with that version)
|
||||
|
||||
// pure JS option
|
||||
// return new vec3(this.x * v2.x, this.y * v2.y, this.z * v2.z);
|
||||
|
||||
// hybrid C++ option
|
||||
return vec3.fromObject(Vec3.multiply(this, v2));
|
||||
},
|
||||
// detects any NaN and Infinity values
|
||||
isValid: function() {
|
||||
return isFinite(this.x) && isFinite(this.y) && isFinite(this.z);
|
||||
},
|
||||
// format Vec3's, eg:
|
||||
// var v = vec3();
|
||||
// print(v); // outputs [Vec3 (0.000, 0.000, 0.000)]
|
||||
toString: function() {
|
||||
if (this === vec3.prototype) {
|
||||
return "{Vec3 prototype}";
|
||||
}
|
||||
function fixed(n) { return n.toFixed(3); }
|
||||
return "[Vec3 (" + [this.x, this.y, this.z].map(fixed) + ")]";
|
||||
},
|
||||
};
|
||||
|
||||
vec3.DEBUG = true;
|
||||
|
||||
function vec3(x, y, z) {
|
||||
if (!(this instanceof vec3)) {
|
||||
// if vec3 is called as a function then re-invoke as a constructor
|
||||
// (so that `value instanceof vec3` holds true for created values)
|
||||
return new vec3(x, y, z);
|
||||
}
|
||||
|
||||
// unfold default arguments (vec3(), vec3(.5), vec3(0,1), etc.)
|
||||
this.x = x !== undefined ? x : 0;
|
||||
this.y = y !== undefined ? y : this.x;
|
||||
this.z = z !== undefined ? z : this.y;
|
||||
|
||||
if (vec3.DEBUG && !this.isValid())
|
||||
throw new Error('vec3() -- invalid initial values ['+[].slice.call(arguments)+']');
|
||||
};
|
||||
|
Loading…
Reference in a new issue