mirror of
https://github.com/lubosz/overte.git
synced 2025-04-10 20:43:39 +02:00
Merge pull request #9685 from humbletim/21114-part2
CR-2 21114 -- BaseScriptEngine.cpp, order-of-operations fixes, reworked JS Exceptions
This commit is contained in:
commit
a744e0d11c
27 changed files with 1126 additions and 240 deletions
|
@ -394,7 +394,7 @@ void EntityScriptServer::selectAudioFormat(const QString& selectedCodecName) {
|
|||
}
|
||||
|
||||
void EntityScriptServer::resetEntitiesScriptEngine() {
|
||||
auto engineName = QString("Entities %1").arg(++_entitiesScriptEngineCount);
|
||||
auto engineName = QString("about:Entities %1").arg(++_entitiesScriptEngineCount);
|
||||
auto newEngine = QSharedPointer<ScriptEngine>(new ScriptEngine(ScriptEngine::ENTITY_SERVER_SCRIPT, NO_SCRIPT, engineName));
|
||||
|
||||
auto webSocketServerConstructorValue = newEngine->newFunction(WebSocketServerClass::constructor);
|
||||
|
@ -477,7 +477,7 @@ void EntityScriptServer::checkAndCallPreload(const EntityItemID& entityID, const
|
|||
if (!scriptUrl.isEmpty()) {
|
||||
scriptUrl = ResourceManager::normalizeURL(scriptUrl);
|
||||
qCDebug(entity_script_server) << "Loading entity server script" << scriptUrl << "for" << entityID;
|
||||
ScriptEngine::loadEntityScript(_entitiesScriptEngine, entityID, scriptUrl, reload);
|
||||
_entitiesScriptEngine->loadEntityScript(entityID, scriptUrl, reload);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -101,7 +101,7 @@ void EntityTreeRenderer::resetEntitiesScriptEngine() {
|
|||
// Keep a ref to oldEngine until newEngine is ready so EntityScriptingInterface has something to use
|
||||
auto oldEngine = _entitiesScriptEngine;
|
||||
|
||||
auto newEngine = new ScriptEngine(ScriptEngine::ENTITY_CLIENT_SCRIPT, NO_SCRIPT, QString("Entities %1").arg(++_entitiesScriptEngineCount));
|
||||
auto newEngine = new ScriptEngine(ScriptEngine::ENTITY_CLIENT_SCRIPT, NO_SCRIPT, QString("about:Entities %1").arg(++_entitiesScriptEngineCount));
|
||||
_entitiesScriptEngine = QSharedPointer<ScriptEngine>(newEngine, entitiesScriptEngineDeleter);
|
||||
|
||||
_scriptingServices->registerScriptEngineWithApplicationServices(_entitiesScriptEngine.data());
|
||||
|
@ -148,7 +148,7 @@ void EntityTreeRenderer::reloadEntityScripts() {
|
|||
_entitiesScriptEngine->unloadAllEntityScripts();
|
||||
foreach(auto entity, _entitiesInScene) {
|
||||
if (!entity->getScript().isEmpty()) {
|
||||
ScriptEngine::loadEntityScript(_entitiesScriptEngine, entity->getEntityItemID(), entity->getScript(), true);
|
||||
_entitiesScriptEngine->loadEntityScript(entity->getEntityItemID(), entity->getScript(), true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -955,7 +955,7 @@ void EntityTreeRenderer::checkAndCallPreload(const EntityItemID& entityID, const
|
|||
}
|
||||
if (shouldLoad && !scriptUrl.isEmpty()) {
|
||||
scriptUrl = ResourceManager::normalizeURL(scriptUrl);
|
||||
ScriptEngine::loadEntityScript(_entitiesScriptEngine, entityID, scriptUrl, reload);
|
||||
_entitiesScriptEngine->loadEntityScript(entityID, scriptUrl, reload);
|
||||
entity->scriptHasPreloaded();
|
||||
}
|
||||
}
|
||||
|
|
287
libraries/script-engine/src/BaseScriptEngine.cpp
Normal file
287
libraries/script-engine/src/BaseScriptEngine.cpp
Normal file
|
@ -0,0 +1,287 @@
|
|||
//
|
||||
// BaseScriptEngine.cpp
|
||||
// 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
|
||||
//
|
||||
|
||||
#include "BaseScriptEngine.h"
|
||||
|
||||
#include <QtCore/QString>
|
||||
#include <QtCore/QThread>
|
||||
#include <QtCore/QUrl>
|
||||
#include <QtScript/QScriptValue>
|
||||
#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 " };
|
||||
|
||||
// engine-aware JS Error copier and factory
|
||||
QScriptValue BaseScriptEngine::makeError(const QScriptValue& _other, const QString& type) {
|
||||
auto other = _other;
|
||||
if (other.isString()) {
|
||||
other = newObject();
|
||||
other.setProperty("message", _other.toString());
|
||||
}
|
||||
auto proto = globalObject().property(type);
|
||||
if (!proto.isFunction()) {
|
||||
proto = globalObject().property(other.prototype().property("constructor").property("name").toString());
|
||||
}
|
||||
if (!proto.isFunction()) {
|
||||
#ifdef DEBUG_JS_EXCEPTIONS
|
||||
qCDebug(scriptengine) << "BaseScriptEngine::makeError -- couldn't find constructor for" << type << " -- using Error instead";
|
||||
#endif
|
||||
proto = globalObject().property("Error");
|
||||
}
|
||||
if (other.engine() != this) {
|
||||
// JS Objects are parented to a specific script engine instance
|
||||
// -- this effectively ~clones it locally by routing through a QVariant and back
|
||||
other = toScriptValue(other.toVariant());
|
||||
}
|
||||
// ~ var err = new Error(other.message)
|
||||
auto err = proto.construct(QScriptValueList({other.property("message")}));
|
||||
|
||||
// transfer over any existing properties
|
||||
QScriptValueIterator it(other);
|
||||
while (it.hasNext()) {
|
||||
it.next();
|
||||
err.setProperty(it.name(), it.value());
|
||||
}
|
||||
return err;
|
||||
}
|
||||
|
||||
// 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) {
|
||||
const auto syntaxCheck = checkSyntax(sourceCode);
|
||||
if (syntaxCheck.state() != QScriptSyntaxCheckResult::Valid) {
|
||||
auto err = globalObject().property("SyntaxError")
|
||||
.construct(QScriptValueList({syntaxCheck.errorMessage()}));
|
||||
err.setProperty("fileName", fileName);
|
||||
err.setProperty("lineNumber", syntaxCheck.errorLineNumber());
|
||||
err.setProperty("expressionBeginOffset", syntaxCheck.errorColumnNumber());
|
||||
err.setProperty("stack", currentContext()->backtrace().join(SCRIPT_BACKTRACE_SEP));
|
||||
{
|
||||
const auto error = syntaxCheck.errorMessage();
|
||||
const auto line = QString::number(syntaxCheck.errorLineNumber());
|
||||
const auto column = QString::number(syntaxCheck.errorColumnNumber());
|
||||
// for compatibility with legacy reporting
|
||||
const auto message = QString("[SyntaxError] %1 in %2:%3(%4)").arg(error, fileName, line, column);
|
||||
err.setProperty("formatted", message);
|
||||
}
|
||||
return err;
|
||||
}
|
||||
return undefinedValue();
|
||||
}
|
||||
|
||||
// this pulls from the best available information to create a detailed snapshot of the current exception
|
||||
QScriptValue BaseScriptEngine::cloneUncaughtException(const QString& extraDetail) {
|
||||
if (!hasUncaughtException()) {
|
||||
return QScriptValue();
|
||||
}
|
||||
auto exception = uncaughtException();
|
||||
// ensure the error object is engine-local
|
||||
auto err = makeError(exception);
|
||||
|
||||
// not sure why Qt does't offer uncaughtExceptionFileName -- but the line number
|
||||
// on its own is often useless/wrong if arbitrarily married to a filename.
|
||||
// when the error object already has this info, it seems to be the most reliable
|
||||
auto fileName = exception.property("fileName").toString();
|
||||
auto lineNumber = exception.property("lineNumber").toInt32();
|
||||
|
||||
// the backtrace, on the other hand, seems most reliable taken from uncaughtExceptionBacktrace
|
||||
auto backtrace = uncaughtExceptionBacktrace();
|
||||
if (backtrace.isEmpty()) {
|
||||
// fallback to the error object
|
||||
backtrace = exception.property("stack").toString().split(SCRIPT_BACKTRACE_SEP);
|
||||
}
|
||||
// the ad hoc "detail" property can be used now to embed additional clues
|
||||
auto detail = exception.property("detail").toString();
|
||||
if (detail.isEmpty()) {
|
||||
detail = extraDetail;
|
||||
} else if (!extraDetail.isEmpty()) {
|
||||
detail += "(" + extraDetail + ")";
|
||||
}
|
||||
if (lineNumber <= 0) {
|
||||
lineNumber = uncaughtExceptionLineNumber();
|
||||
}
|
||||
if (fileName.isEmpty()) {
|
||||
// climb the stack frames looking for something useful to display
|
||||
for (auto c = currentContext(); c && fileName.isEmpty(); c = c->parentContext()) {
|
||||
QScriptContextInfo info { c };
|
||||
if (!info.fileName().isEmpty()) {
|
||||
// take fileName:lineNumber as a pair
|
||||
fileName = info.fileName();
|
||||
lineNumber = info.lineNumber();
|
||||
if (backtrace.isEmpty()) {
|
||||
backtrace = c->backtrace();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
err.setProperty("fileName", fileName);
|
||||
err.setProperty("lineNumber", lineNumber );
|
||||
err.setProperty("detail", detail);
|
||||
err.setProperty("stack", backtrace.join(SCRIPT_BACKTRACE_SEP));
|
||||
|
||||
#ifdef DEBUG_JS_EXCEPTIONS
|
||||
err.setProperty("_fileName", exception.property("fileName").toString());
|
||||
err.setProperty("_stack", uncaughtExceptionBacktrace().join(SCRIPT_BACKTRACE_SEP));
|
||||
err.setProperty("_lineNumber", uncaughtExceptionLineNumber());
|
||||
#endif
|
||||
return err;
|
||||
}
|
||||
|
||||
QString BaseScriptEngine::formatException(const QScriptValue& exception) {
|
||||
QString note { "UncaughtException" };
|
||||
QString result;
|
||||
|
||||
if (!exception.isObject()) {
|
||||
return result;
|
||||
}
|
||||
const auto message = exception.toString();
|
||||
const auto fileName = exception.property("fileName").toString();
|
||||
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.
|
||||
// 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...
|
||||
if (exception.property("detail").isValid()) {
|
||||
note += " " + exception.property("detail").toString();
|
||||
}
|
||||
}
|
||||
|
||||
result = QString(SCRIPT_EXCEPTION_FORMAT).arg(note, message, fileName, lineNumber);
|
||||
if (!stacktrace.isEmpty()) {
|
||||
result += QString("\n[Backtrace]%1%2").arg(SCRIPT_BACKTRACE_SEP).arg(stacktrace);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
const auto fileName = program.fileName();
|
||||
const auto shortName = QUrl(fileName).fileName();
|
||||
|
||||
QScriptValue result;
|
||||
QScriptValue oldGlobal;
|
||||
auto global = closure.property("global");
|
||||
if (global.isObject()) {
|
||||
#ifdef DEBUG_JS
|
||||
qCDebug(scriptengine) << " setting global = closure.global" << shortName;
|
||||
#endif
|
||||
oldGlobal = globalObject();
|
||||
setGlobalObject(global);
|
||||
}
|
||||
|
||||
auto context = pushContext();
|
||||
|
||||
auto thiz = closure.property("this");
|
||||
if (thiz.isObject()) {
|
||||
#ifdef DEBUG_JS
|
||||
qCDebug(scriptengine) << " 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);
|
||||
#endif
|
||||
{
|
||||
result = BaseScriptEngine::evaluate(program);
|
||||
if (hasUncaughtException()) {
|
||||
auto err = cloneUncaughtException(__FUNCTION__);
|
||||
#ifdef DEBUG_JS_EXCEPTIONS
|
||||
qCWarning(scriptengine) << __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);
|
||||
#endif
|
||||
popContext();
|
||||
|
||||
if (oldGlobal.isValid()) {
|
||||
#ifdef DEBUG_JS
|
||||
qCDebug(scriptengine) << " restoring global" << shortName;
|
||||
#endif
|
||||
setGlobalObject(oldGlobal);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// 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);
|
||||
auto call = object.property("call");
|
||||
call.setPrototype(object); // context->callee().prototype() === Lambda QObject
|
||||
call.setData(data); // context->callee().data() will === data param
|
||||
return call;
|
||||
}
|
||||
QString Lambda::toString() const {
|
||||
return QString("[Lambda%1]").arg(data.isValid() ? " " + data.toString() : data.toString());
|
||||
}
|
||||
|
||||
Lambda::~Lambda() {
|
||||
#ifdef DEBUG_JS_LAMBDA_FUNCS
|
||||
qDebug() << "~Lambda" << "this" << this;
|
||||
#endif
|
||||
}
|
||||
|
||||
Lambda::Lambda(QScriptEngine *engine, std::function<QScriptValue(QScriptContext *, QScriptEngine*)> operation, QScriptValue data)
|
||||
: engine(engine), operation(operation), data(data) {
|
||||
#ifdef DEBUG_JS_LAMBDA_FUNCS
|
||||
qDebug() << "Lambda" << data.toString();
|
||||
#endif
|
||||
}
|
||||
QScriptValue Lambda::call() {
|
||||
return operation(engine->currentContext(), engine);
|
||||
}
|
||||
|
||||
#ifdef DEBUG_JS
|
||||
void BaseScriptEngine::_debugDump(const QString& header, const QScriptValue& object, const QString& footer) {
|
||||
if (!header.isEmpty()) {
|
||||
qCDebug(scriptengine) << header;
|
||||
}
|
||||
if (!object.isObject()) {
|
||||
qCDebug(scriptengine) << "(!isObject)" << object.toVariant().toString() << object.toString();
|
||||
return;
|
||||
}
|
||||
QScriptValueIterator it(object);
|
||||
while (it.hasNext()) {
|
||||
it.next();
|
||||
qCDebug(scriptengine) << it.name() << ":" << it.value().toString();
|
||||
}
|
||||
if (!footer.isEmpty()) {
|
||||
qCDebug(scriptengine) << footer;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
67
libraries/script-engine/src/BaseScriptEngine.h
Normal file
67
libraries/script-engine/src/BaseScriptEngine.h
Normal file
|
@ -0,0 +1,67 @@
|
|||
//
|
||||
// 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
|
|
@ -66,7 +66,7 @@ void BatchLoader::start(int maxRetries) {
|
|||
qCDebug(scriptengine) << "Loaded: " << url;
|
||||
} else {
|
||||
_data.insert(url, QString());
|
||||
qCDebug(scriptengine) << "Could not load: " << url;
|
||||
qCDebug(scriptengine) << "Could not load: " << url << status;
|
||||
}
|
||||
|
||||
if (!_finished && _urls.size() == _data.size()) {
|
||||
|
|
|
@ -188,6 +188,8 @@ void ScriptCache::scriptContentAvailable(int maxRetries) {
|
|||
|
||||
}
|
||||
}
|
||||
} else {
|
||||
qCWarning(scriptengine) << "Warning: scriptContentAvailable for inactive url: " << url;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -43,6 +43,9 @@ class ScriptCache : public QObject, public Dependency {
|
|||
public:
|
||||
static const QString STATUS_INLINE;
|
||||
static const QString STATUS_CACHED;
|
||||
static bool isSuccessStatus(const QString& status) {
|
||||
return status == "Success" || status == STATUS_INLINE || status == STATUS_CACHED;
|
||||
}
|
||||
|
||||
void clearCache();
|
||||
Q_INVOKABLE void clearATPScriptsFromCache();
|
||||
|
|
|
@ -50,6 +50,7 @@
|
|||
|
||||
#include "ArrayBufferViewClass.h"
|
||||
#include "BatchLoader.h"
|
||||
#include "BaseScriptEngine.h"
|
||||
#include "DataViewClass.h"
|
||||
#include "EventTypes.h"
|
||||
#include "FileScriptingInterface.h" // unzip project
|
||||
|
@ -69,9 +70,12 @@
|
|||
|
||||
#include "MIDIEvent.h"
|
||||
|
||||
const QString BaseScriptEngine::SCRIPT_EXCEPTION_FORMAT { "[UncaughtException] %1 in %2:%3" };
|
||||
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 };
|
||||
|
||||
|
@ -90,13 +94,9 @@ static QScriptValue debugPrint(QScriptContext* context, QScriptEngine* engine){
|
|||
}
|
||||
qCDebug(scriptengineScript).noquote() << "script:print()<<" << message; // noquote() so that \n is treated as newline
|
||||
|
||||
message = message.replace("\\", "\\\\")
|
||||
.replace("\n", "\\n")
|
||||
.replace("\r", "\\r")
|
||||
.replace("'", "\\'");
|
||||
|
||||
// FIXME - this approach neeeds revisiting. print() comes here, which ends up doing an evaluate?
|
||||
engine->evaluate("Script.print('" + message + "')");
|
||||
// FIXME - this approach neeeds revisiting. print() comes here, which ends up calling Script.print?
|
||||
engine->globalObject().property("Script").property("print")
|
||||
.call(engine->nullValue(), QScriptValueList({ message }));
|
||||
|
||||
return QScriptValue();
|
||||
}
|
||||
|
@ -140,52 +140,15 @@ QString encodeEntityIdIntoEntityUrl(const QString& url, const QString& entityID)
|
|||
return url + " [EntityID:" + entityID + "]";
|
||||
}
|
||||
|
||||
QString BaseScriptEngine::lintScript(const QString& sourceCode, const QString& fileName, const int lineNumber) {
|
||||
const auto syntaxCheck = checkSyntax(sourceCode);
|
||||
if (syntaxCheck.state() != syntaxCheck.Valid) {
|
||||
const auto error = syntaxCheck.errorMessage();
|
||||
const auto line = QString::number(syntaxCheck.errorLineNumber());
|
||||
const auto column = QString::number(syntaxCheck.errorColumnNumber());
|
||||
const auto message = QString("[SyntaxError] %1 in %2:%3(%4)").arg(error, fileName, line, column);
|
||||
return message;
|
||||
}
|
||||
return QString();
|
||||
}
|
||||
|
||||
QString BaseScriptEngine::formatUncaughtException(const QString& overrideFileName) {
|
||||
QString message;
|
||||
if (hasUncaughtException()) {
|
||||
const auto error = uncaughtException();
|
||||
const auto backtrace = uncaughtExceptionBacktrace();
|
||||
const auto exception = error.toString();
|
||||
auto filename = overrideFileName;
|
||||
if (filename.isEmpty()) {
|
||||
QScriptContextInfo ctx { currentContext() };
|
||||
filename = ctx.fileName();
|
||||
}
|
||||
const auto line = QString::number(uncaughtExceptionLineNumber());
|
||||
|
||||
message = QString(SCRIPT_EXCEPTION_FORMAT).arg(exception, overrideFileName, line);
|
||||
if (!backtrace.empty()) {
|
||||
static const auto lineSeparator = "\n ";
|
||||
message += QString("\n[Backtrace]%1%2").arg(lineSeparator, backtrace.join(lineSeparator));
|
||||
}
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
QString ScriptEngine::reportUncaughtException(const QString& overrideFileName) {
|
||||
QString message;
|
||||
if (!hasUncaughtException()) {
|
||||
return message;
|
||||
}
|
||||
message = formatUncaughtException(overrideFileName.isEmpty() ? _fileNameString : overrideFileName);
|
||||
QString ScriptEngine::logException(const QScriptValue& exception) {
|
||||
auto message = formatException(exception);
|
||||
scriptErrorMessage(qPrintable(message));
|
||||
return message;
|
||||
}
|
||||
|
||||
int ScriptEngine::processLevelMaxRetries { ScriptRequest::MAX_RETRIES };
|
||||
ScriptEngine::ScriptEngine(Context context, const QString& scriptContents, const QString& fileNameString) :
|
||||
BaseScriptEngine(),
|
||||
_context(context),
|
||||
_scriptContents(scriptContents),
|
||||
_timerFunctionMap(),
|
||||
|
@ -195,16 +158,30 @@ ScriptEngine::ScriptEngine(Context context, const QString& scriptContents, const
|
|||
DependencyManager::get<ScriptEngines>()->addScriptEngine(this);
|
||||
|
||||
connect(this, &QScriptEngine::signalHandlerException, this, [this](const QScriptValue& exception) {
|
||||
reportUncaughtException();
|
||||
clearExceptions();
|
||||
});
|
||||
if (hasUncaughtException()) {
|
||||
// the engine's uncaughtException() seems to produce much better stack traces here
|
||||
emit unhandledException(cloneUncaughtException("signalHandlerException"));
|
||||
clearExceptions();
|
||||
} else {
|
||||
// ... but may not always be available -- so if needed we fallback to the passed exception
|
||||
emit unhandledException(exception);
|
||||
}
|
||||
}, Qt::DirectConnection);
|
||||
|
||||
setProcessEventsInterval(MSECS_PER_SECOND);
|
||||
if (isEntityServerScript()) {
|
||||
qCDebug(scriptengine) << "isEntityServerScript() -- limiting maxRetries to 1";
|
||||
processLevelMaxRetries = 1;
|
||||
}
|
||||
qCDebug(scriptengine) << getContext() << "processLevelMaxRetries =" << processLevelMaxRetries;
|
||||
|
||||
// this is where all unhandled exceptions end up getting logged
|
||||
connect(this, &BaseScriptEngine::unhandledException, this, [this](const QScriptValue& err) {
|
||||
auto output = err.engine() == this ? err : makeError(err);
|
||||
if (!output.property("detail").isValid()) {
|
||||
output.setProperty("detail", "UnhandledException");
|
||||
}
|
||||
logException(output);
|
||||
});
|
||||
}
|
||||
|
||||
QString ScriptEngine::getContext() const {
|
||||
|
@ -224,13 +201,22 @@ QString ScriptEngine::getContext() const {
|
|||
}
|
||||
|
||||
ScriptEngine::~ScriptEngine() {
|
||||
// FIXME: are these scriptInfoMessage/scriptWarningMessage segfaulting anybody else at app shutdown?
|
||||
#if !defined(Q_OS_LINUX)
|
||||
scriptInfoMessage("Script Engine shutting down:" + getFilename());
|
||||
#else
|
||||
qCDebug(scriptengine) << "~ScriptEngine()" << this;
|
||||
#endif
|
||||
|
||||
auto scriptEngines = DependencyManager::get<ScriptEngines>();
|
||||
if (scriptEngines) {
|
||||
scriptEngines->removeScriptEngine(this);
|
||||
} else {
|
||||
#if !defined(Q_OS_LINUX)
|
||||
scriptWarningMessage("Script destroyed after ScriptEngines!");
|
||||
#else
|
||||
qCWarning(scriptengine) << ("Script destroyed after ScriptEngines!");
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -320,9 +306,12 @@ void ScriptEngine::runDebuggable() {
|
|||
}
|
||||
}
|
||||
_lastUpdate = now;
|
||||
// Debug and clear exceptions
|
||||
if (hasUncaughtException()) {
|
||||
reportUncaughtException();
|
||||
|
||||
// only clear exceptions if we are not in the middle of evaluating
|
||||
if (!isEvaluating() && hasUncaughtException()) {
|
||||
qCWarning(scriptengine) << __FUNCTION__ << "---------- UNCAUGHT EXCEPTION --------";
|
||||
qCWarning(scriptengine) << "runDebuggable" << uncaughtException().toString();
|
||||
logException(__FUNCTION__);
|
||||
clearExceptions();
|
||||
}
|
||||
});
|
||||
|
@ -357,10 +346,9 @@ void ScriptEngine::runInThread() {
|
|||
workerThread->start();
|
||||
}
|
||||
|
||||
void ScriptEngine::executeOnScriptThread(std::function<void()> function, bool blocking ) {
|
||||
void ScriptEngine::executeOnScriptThread(std::function<void()> function, const Qt::ConnectionType& type ) {
|
||||
if (QThread::currentThread() != thread()) {
|
||||
QMetaObject::invokeMethod(this, "executeOnScriptThread", blocking ? Qt::BlockingQueuedConnection : Qt::QueuedConnection,
|
||||
Q_ARG(std::function<void()>, function));
|
||||
QMetaObject::invokeMethod(this, "executeOnScriptThread", type, Q_ARG(std::function<void()>, function));
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -559,6 +547,7 @@ void ScriptEngine::init() {
|
|||
qCWarning(scriptengine) << "deletingEntity while entity script is still running!" << entityID;
|
||||
}
|
||||
_entityScripts.remove(entityID);
|
||||
emit entityScriptDetailsUpdated();
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -591,8 +580,7 @@ void ScriptEngine::init() {
|
|||
QScriptValue webSocketConstructorValue = newFunction(WebSocketClass::constructor);
|
||||
globalObject().setProperty("WebSocket", webSocketConstructorValue);
|
||||
|
||||
QScriptValue printConstructorValue = newFunction(debugPrint);
|
||||
globalObject().setProperty("print", printConstructorValue);
|
||||
globalObject().setProperty("print", newFunction(debugPrint));
|
||||
|
||||
QScriptValue audioEffectOptionsConstructorValue = newFunction(AudioEffectOptions::constructor);
|
||||
globalObject().setProperty("AudioEffectOptions", audioEffectOptionsConstructorValue);
|
||||
|
@ -606,6 +594,7 @@ void ScriptEngine::init() {
|
|||
qScriptRegisterMetaType(this, wscReadyStateToScriptValue, wscReadyStateFromScriptValue);
|
||||
|
||||
registerGlobalObject("Script", this);
|
||||
|
||||
registerGlobalObject("Audio", &AudioScriptingInterface::getInstance());
|
||||
registerGlobalObject("Entities", entityScriptingInterface.data());
|
||||
registerGlobalObject("Quat", &_quatLibrary);
|
||||
|
@ -874,7 +863,6 @@ void ScriptEngine::addEventHandler(const EntityItemID& entityID, const QString&
|
|||
handlersForEvent << handlerData; // Note that the same handler can be added many times. See removeEntityEventHandler().
|
||||
}
|
||||
|
||||
|
||||
QScriptValue ScriptEngine::evaluate(const QString& sourceCode, const QString& fileName, int lineNumber) {
|
||||
if (DependencyManager::get<ScriptEngines>()->isStopped()) {
|
||||
return QScriptValue(); // bail early
|
||||
|
@ -896,23 +884,30 @@ 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 {
|
||||
syntaxError.setProperty("detail", "evaluate");
|
||||
emit unhandledException(syntaxError);
|
||||
}
|
||||
return syntaxError;
|
||||
}
|
||||
QScriptProgram program { sourceCode, fileName, lineNumber };
|
||||
if (!syntaxError.isEmpty() || program.isNull()) {
|
||||
scriptErrorMessage(qPrintable(syntaxError));
|
||||
return QScriptValue();
|
||||
if (program.isNull()) {
|
||||
// can this happen?
|
||||
auto err = makeError("could not create QScriptProgram for " + fileName);
|
||||
emit unhandledException(err);
|
||||
return err;
|
||||
}
|
||||
|
||||
++_evaluatesPending;
|
||||
auto result = BaseScriptEngine::evaluate(program);
|
||||
--_evaluatesPending;
|
||||
|
||||
if (hasUncaughtException()) {
|
||||
result = uncaughtException();
|
||||
reportUncaughtException(program.fileName());
|
||||
emit evaluationFinished(result, true);
|
||||
clearExceptions();
|
||||
} else {
|
||||
emit evaluationFinished(result, false);
|
||||
QScriptValue result;
|
||||
{
|
||||
result = BaseScriptEngine::evaluate(program);
|
||||
if (!isEvaluating() && hasUncaughtException()) {
|
||||
emit unhandledException(cloneUncaughtException(__FUNCTION__));
|
||||
clearExceptions();
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
@ -933,8 +928,13 @@ void ScriptEngine::run() {
|
|||
_isRunning = true;
|
||||
emit runningStateChanged();
|
||||
|
||||
QScriptValue result = evaluate(_scriptContents, _fileNameString);
|
||||
|
||||
{
|
||||
evaluate(_scriptContents, _fileNameString);
|
||||
if (!isEvaluating() && hasUncaughtException()) {
|
||||
emit unhandledException(cloneUncaughtException(__FUNCTION__));
|
||||
clearExceptions();
|
||||
}
|
||||
}
|
||||
#ifdef _WIN32
|
||||
// VS13 does not sleep_until unless it uses the system_clock, see:
|
||||
// https://www.reddit.com/r/cpp_questions/comments/3o71ic/sleep_until_not_working_with_a_time_pointsteady/
|
||||
|
@ -1061,13 +1061,14 @@ void ScriptEngine::run() {
|
|||
}
|
||||
_lastUpdate = now;
|
||||
|
||||
// Debug and clear exceptions
|
||||
if (hasUncaughtException()) {
|
||||
reportUncaughtException();
|
||||
// only clear exceptions if we are not in the middle of evaluating
|
||||
if (!isEvaluating() && hasUncaughtException()) {
|
||||
qCWarning(scriptengine) << __FUNCTION__ << "---------- UNCAUGHT EXCEPTION --------";
|
||||
qCWarning(scriptengine) << "runInThread" << uncaughtException().toString();
|
||||
emit unhandledException(cloneUncaughtException(__FUNCTION__));
|
||||
clearExceptions();
|
||||
}
|
||||
}
|
||||
|
||||
scriptInfoMessage("Script Engine stopping:" + getFilename());
|
||||
|
||||
stopAllTimers(); // make sure all our timers are stopped if the script is ending
|
||||
|
@ -1100,9 +1101,11 @@ void ScriptEngine::run() {
|
|||
// we want to only call it in our own run "shutdown" processing.
|
||||
void ScriptEngine::stopAllTimers() {
|
||||
QMutableHashIterator<QTimer*, CallbackData> i(_timerFunctionMap);
|
||||
int j {0};
|
||||
while (i.hasNext()) {
|
||||
i.next();
|
||||
QTimer* timer = i.key();
|
||||
qCDebug(scriptengine) << getFilename() << "stopAllTimers[" << j++ << "]";
|
||||
stopTimer(timer);
|
||||
}
|
||||
}
|
||||
|
@ -1197,11 +1200,11 @@ void ScriptEngine::timerFired() {
|
|||
auto postTimer = p_high_resolution_clock::now();
|
||||
auto elapsed = (postTimer - preTimer);
|
||||
_totalTimerExecution += std::chrono::duration_cast<std::chrono::microseconds>(elapsed);
|
||||
|
||||
} else {
|
||||
qCWarning(scriptengine) << "timerFired -- invalid function" << timerData.function.toVariant().toString();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
QObject* ScriptEngine::setupTimerWithInterval(const QScriptValue& function, int intervalMS, bool isSingleShot) {
|
||||
// create the timer, add it to the map, and start it
|
||||
QTimer* newTimer = new QTimer(this);
|
||||
|
@ -1218,7 +1221,7 @@ QObject* ScriptEngine::setupTimerWithInterval(const QScriptValue& function, int
|
|||
// make sure the timer stops when the script does
|
||||
connect(this, &ScriptEngine::scriptEnding, newTimer, &QTimer::stop);
|
||||
|
||||
CallbackData timerData = {function, currentEntityIdentifier, currentSandboxURL };
|
||||
CallbackData timerData = { function, currentEntityIdentifier, currentSandboxURL };
|
||||
_timerFunctionMap.insert(newTimer, timerData);
|
||||
|
||||
newTimer->start(intervalMS);
|
||||
|
@ -1248,33 +1251,44 @@ void ScriptEngine::stopTimer(QTimer *timer) {
|
|||
timer->stop();
|
||||
_timerFunctionMap.remove(timer);
|
||||
delete timer;
|
||||
} else {
|
||||
qCDebug(scriptengine) << "stopTimer -- not in _timerFunctionMap" << timer;
|
||||
}
|
||||
}
|
||||
|
||||
QUrl ScriptEngine::resolvePath(const QString& include) const {
|
||||
QUrl url(include);
|
||||
// first lets check to see if it's already a full URL
|
||||
if (!url.scheme().isEmpty()) {
|
||||
// first lets check to see if it's already a full URL -- or a Windows path like "c:/"
|
||||
if (include.startsWith("/") || url.scheme().length() == 1) {
|
||||
url = QUrl::fromLocalFile(include);
|
||||
}
|
||||
if (!url.isRelative()) {
|
||||
return expandScriptUrl(url);
|
||||
}
|
||||
|
||||
QScriptContextInfo contextInfo { currentContext()->parentContext() };
|
||||
|
||||
|
||||
// we apparently weren't a fully qualified url, so, let's assume we're relative
|
||||
// to the original URL of our script
|
||||
QUrl parentURL = contextInfo.fileName();
|
||||
if (parentURL.isEmpty()) {
|
||||
if (_parentURL.isEmpty()) {
|
||||
parentURL = QUrl(_fileNameString);
|
||||
} else {
|
||||
parentURL = QUrl(_parentURL);
|
||||
}
|
||||
// to the first absolute URL in the JS scope chain
|
||||
QUrl parentURL;
|
||||
auto context = currentContext();
|
||||
do {
|
||||
QScriptContextInfo contextInfo { context };
|
||||
parentURL = QUrl(contextInfo.fileName());
|
||||
context = context->parentContext();
|
||||
} while (parentURL.isRelative() && context);
|
||||
|
||||
if (parentURL.isRelative()) {
|
||||
// fallback to the "include" parent (if defined, this will already be absolute)
|
||||
parentURL = QUrl(_parentURL);
|
||||
}
|
||||
|
||||
// if the parent URL's scheme is empty, then this is probably a local file...
|
||||
if (parentURL.scheme().isEmpty()) {
|
||||
parentURL = QUrl::fromLocalFile(_fileNameString);
|
||||
if (parentURL.isRelative()) {
|
||||
// fallback to the original script engine URL
|
||||
parentURL = QUrl(_fileNameString);
|
||||
|
||||
// if still relative and path-like, then this is probably a local file...
|
||||
if (parentURL.isRelative() && url.path().contains("/")) {
|
||||
parentURL = QUrl::fromLocalFile(_fileNameString);
|
||||
}
|
||||
}
|
||||
|
||||
// at this point we should have a legitimate fully qualified URL for our parent
|
||||
|
@ -1301,22 +1315,7 @@ void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callbac
|
|||
return; // bail early
|
||||
}
|
||||
QList<QUrl> urls;
|
||||
bool knowsSensitivity = false;
|
||||
Qt::CaseSensitivity sensitivity { Qt::CaseSensitive };
|
||||
auto getSensitivity = [&]() {
|
||||
if (!knowsSensitivity) {
|
||||
QString path = currentSandboxURL.path();
|
||||
QFileInfo upperFI(path.toUpper());
|
||||
QFileInfo lowerFI(path.toLower());
|
||||
sensitivity = (upperFI == lowerFI) ? Qt::CaseInsensitive : Qt::CaseSensitive;
|
||||
knowsSensitivity = true;
|
||||
}
|
||||
return sensitivity;
|
||||
};
|
||||
|
||||
// Guard against meaningless query and fragment parts.
|
||||
// Do NOT use PreferLocalFile as its behavior is unpredictable (e.g., on defaultScriptsLocation())
|
||||
const auto strippingFlags = QUrl::RemoveFilename | QUrl::RemoveQuery | QUrl::RemoveFragment;
|
||||
for (QString includeFile : includeFiles) {
|
||||
QString file = ResourceManager::normalizeURL(includeFile);
|
||||
QUrl thisURL;
|
||||
|
@ -1333,10 +1332,8 @@ void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callbac
|
|||
thisURL = resolvePath(file);
|
||||
}
|
||||
|
||||
if (!isStandardLibrary && !currentSandboxURL.isEmpty() && (thisURL.scheme() == "file") &&
|
||||
(currentSandboxURL.scheme() != "file" ||
|
||||
!thisURL.toString(strippingFlags).startsWith(currentSandboxURL.toString(strippingFlags), getSensitivity()))) {
|
||||
|
||||
bool disallowOutsideFiles = thisURL.isLocalFile() && !isStandardLibrary && !currentSandboxURL.isLocalFile();
|
||||
if (disallowOutsideFiles && !PathUtils::isDescendantOf(thisURL, currentSandboxURL)) {
|
||||
scriptWarningMessage("Script.include() ignoring file path" + thisURL.toString()
|
||||
+ "outside of original entity script" + currentSandboxURL.toString());
|
||||
} else {
|
||||
|
@ -1373,6 +1370,10 @@ void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callbac
|
|||
};
|
||||
|
||||
doWithEnvironment(capturedEntityIdentifier, capturedSandboxURL, operation);
|
||||
if (hasUncaughtException()) {
|
||||
emit unhandledException(cloneUncaughtException(__FUNCTION__));
|
||||
clearExceptions();
|
||||
}
|
||||
} else {
|
||||
scriptWarningMessage("Script.include() skipping evaluation of previously included url:" + url.toString());
|
||||
}
|
||||
|
@ -1474,21 +1475,6 @@ int ScriptEngine::getNumRunningEntityScripts() const {
|
|||
return sum;
|
||||
}
|
||||
|
||||
QString ScriptEngine::getEntityScriptStatus(const EntityItemID& entityID) {
|
||||
if (_entityScripts.contains(entityID))
|
||||
return EntityScriptStatus_::valueToKey(_entityScripts[entityID].status).toLower();
|
||||
return QString();
|
||||
}
|
||||
|
||||
bool ScriptEngine::getEntityScriptDetails(const EntityItemID& entityID, EntityScriptDetails &details) const {
|
||||
auto it = _entityScripts.constFind(entityID);
|
||||
if (it == _entityScripts.constEnd()) {
|
||||
return false;
|
||||
}
|
||||
details = it.value();
|
||||
return true;
|
||||
}
|
||||
|
||||
void ScriptEngine::setEntityScriptDetails(const EntityItemID& entityID, const EntityScriptDetails& details) {
|
||||
_entityScripts[entityID] = details;
|
||||
emit entityScriptDetailsUpdated();
|
||||
|
@ -1501,31 +1487,175 @@ void ScriptEngine::updateEntityScriptStatus(const EntityItemID& entityID, const
|
|||
emit entityScriptDetailsUpdated();
|
||||
}
|
||||
|
||||
// since all of these operations can be asynch we will always do the actual work in the response handler
|
||||
// for the download
|
||||
void ScriptEngine::loadEntityScript(QWeakPointer<ScriptEngine> theEngine, const EntityItemID& entityID, const QString& entityScript, bool forceRedownload) {
|
||||
auto engine = theEngine.data();
|
||||
engine->executeOnScriptThread([=]{
|
||||
EntityScriptDetails details = engine->_entityScripts[entityID];
|
||||
if (details.status == EntityScriptStatus::PENDING || details.status == EntityScriptStatus::UNLOADED) {
|
||||
engine->updateEntityScriptStatus(entityID, EntityScriptStatus::LOADING, QThread::currentThread()->objectName());
|
||||
}
|
||||
});
|
||||
bool ScriptEngine::getEntityScriptDetails(const EntityItemID& entityID, EntityScriptDetails &details) const {
|
||||
auto it = _entityScripts.constFind(entityID);
|
||||
if (it == _entityScripts.constEnd()) {
|
||||
return false;
|
||||
}
|
||||
details = it.value();
|
||||
return true;
|
||||
}
|
||||
|
||||
// NOTE: If the script content is not currently in the cache, the LAMBDA here will be called on the Main Thread
|
||||
// which means we're guaranteed that it's not the correct thread for the ScriptEngine. This means
|
||||
// when we get into entityScriptContentAvailable() we will likely invokeMethod() to get it over
|
||||
// to the "Entities" ScriptEngine thread.
|
||||
DependencyManager::get<ScriptCache>()->getScriptContents(entityScript, [theEngine, entityID](const QString& scriptOrURL, const QString& contents, bool isURL, bool success, const QString &status) {
|
||||
QSharedPointer<ScriptEngine> strongEngine = theEngine.toStrongRef();
|
||||
if (strongEngine) {
|
||||
#ifdef THREAD_DEBUGGING
|
||||
qCDebug(scriptengine) << "ScriptEngine::entityScriptContentAvailable() IN LAMBDA contentAvailable on thread ["
|
||||
<< QThread::currentThread() << "] expected thread [" << strongEngine->thread() << "]";
|
||||
#endif
|
||||
strongEngine->entityScriptContentAvailable(entityID, scriptOrURL, contents, isURL, success, status);
|
||||
const static EntityItemID BAD_SCRIPT_UUID_PLACEHOLDER { "{20170224-dead-face-0000-cee000021114}" };
|
||||
|
||||
void ScriptEngine::processDeferredEntityLoads(const QString& entityScript, const EntityItemID& leaderID) {
|
||||
QList<DeferredLoadEntity> retryLoads;
|
||||
QMutableListIterator<DeferredLoadEntity> i(_deferredEntityLoads);
|
||||
while (i.hasNext()) {
|
||||
auto retry = i.next();
|
||||
if (retry.entityScript == entityScript) {
|
||||
retryLoads << retry;
|
||||
i.remove();
|
||||
}
|
||||
}, forceRedownload, processLevelMaxRetries);
|
||||
}
|
||||
foreach(DeferredLoadEntity retry, retryLoads) {
|
||||
// check whether entity was since been deleted
|
||||
if (!_entityScripts.contains(retry.entityID)) {
|
||||
qCDebug(scriptengine) << "processDeferredEntityLoads -- entity details gone (entity deleted?)"
|
||||
<< retry.entityID;
|
||||
continue;
|
||||
}
|
||||
|
||||
// check whether entity has since been unloaded or otherwise errored-out
|
||||
auto details = _entityScripts[retry.entityID];
|
||||
if (details.status != EntityScriptStatus::PENDING) {
|
||||
qCDebug(scriptengine) << "processDeferredEntityLoads -- entity status no longer PENDING; "
|
||||
<< retry.entityID << details.status;
|
||||
continue;
|
||||
}
|
||||
|
||||
// propagate leader's failure reasons to the pending entity
|
||||
const auto leaderDetails = _entityScripts[leaderID];
|
||||
if (leaderDetails.status != EntityScriptStatus::RUNNING) {
|
||||
qCDebug(scriptengine) << QString("... pending load of %1 cancelled (leader: %2 status: %3)")
|
||||
.arg(retry.entityID.toString()).arg(leaderID.toString()).arg(leaderDetails.status);
|
||||
|
||||
auto extraDetail = QString("\n(propagated from %1)").arg(leaderID.toString());
|
||||
if (leaderDetails.status == EntityScriptStatus::ERROR_LOADING_SCRIPT ||
|
||||
leaderDetails.status == EntityScriptStatus::ERROR_RUNNING_SCRIPT) {
|
||||
// propagate same error so User doesn't have to hunt down stampede's leader
|
||||
updateEntityScriptStatus(retry.entityID, leaderDetails.status, leaderDetails.errorInfo + extraDetail);
|
||||
} else {
|
||||
// the leader Entity somehow ended up in some other state (rapid-fire delete or unload could cause)
|
||||
updateEntityScriptStatus(retry.entityID, EntityScriptStatus::ERROR_LOADING_SCRIPT,
|
||||
"A previous Entity failed to load using this script URL; reload to try again." + extraDetail);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_occupiedScriptURLs.contains(retry.entityScript)) {
|
||||
qCWarning(scriptengine) << "--- SHOULD NOT HAPPEN -- recursive call into processDeferredEntityLoads" << retry.entityScript;
|
||||
continue;
|
||||
}
|
||||
|
||||
// if we made it here then the leading entity was successful so proceed with normal load
|
||||
loadEntityScript(retry.entityID, retry.entityScript, false);
|
||||
}
|
||||
}
|
||||
|
||||
void ScriptEngine::loadEntityScript(const EntityItemID& entityID, const QString& entityScript, bool forceRedownload) {
|
||||
if (QThread::currentThread() != thread()) {
|
||||
QMetaObject::invokeMethod(this, "loadEntityScript",
|
||||
Q_ARG(const EntityItemID&, entityID),
|
||||
Q_ARG(const QString&, entityScript),
|
||||
Q_ARG(bool, forceRedownload)
|
||||
);
|
||||
return;
|
||||
}
|
||||
PROFILE_RANGE(script, __FUNCTION__);
|
||||
|
||||
if (isStopping() || DependencyManager::get<ScriptEngines>()->isStopped()) {
|
||||
qCDebug(scriptengine) << "loadEntityScript.start " << entityScript << entityID.toString()
|
||||
<< " but isStopping==" << isStopping()
|
||||
<< " || engines->isStopped==" << DependencyManager::get<ScriptEngines>()->isStopped();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_entityScripts.contains(entityID)) {
|
||||
// make sure EntityScriptDetails has an entry for this UUID right away
|
||||
// (which allows bailing from the loading/provisioning process early if the Entity gets deleted mid-flight)
|
||||
updateEntityScriptStatus(entityID, EntityScriptStatus::PENDING, "...pending...");
|
||||
}
|
||||
|
||||
// This "occupied" approach allows multiple Entities to boot from the same script URL while still taking
|
||||
// full advantage of cacheable require modules. This only affects Entities literally coming in back-to-back
|
||||
// before the first one has time to finish loading.
|
||||
if (_occupiedScriptURLs.contains(entityScript)) {
|
||||
auto currentEntityID = _occupiedScriptURLs[entityScript];
|
||||
if (currentEntityID == BAD_SCRIPT_UUID_PLACEHOLDER) {
|
||||
if (forceRedownload) {
|
||||
// script was previously marked unusable, but we're reloading so reset it
|
||||
_occupiedScriptURLs.remove(entityScript);
|
||||
} else {
|
||||
// since not reloading, assume that the exact same input would produce the exact same output again
|
||||
// note: this state gets reset with "reload all scripts," leaving/returning to a Domain, clear cache, etc.
|
||||
#ifdef DEBUG_ENTITY_STATES
|
||||
qCDebug(scriptengine) << QString("loadEntityScript.cancelled entity: %1 script: %2 (previous script failure)")
|
||||
.arg(entityID.toString()).arg(entityScript);
|
||||
#endif
|
||||
updateEntityScriptStatus(entityID, EntityScriptStatus::ERROR_LOADING_SCRIPT,
|
||||
"A previous Entity failed to load using this script URL; reload to try again.");
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// another entity is busy loading from this script URL so wait for them to finish
|
||||
#ifdef DEBUG_ENTITY_STATES
|
||||
qCDebug(scriptengine) << QString("loadEntityScript.deferring[%0] entity: %1 script: %2 (waiting on %3)")
|
||||
.arg(_deferredEntityLoads.size()).arg(entityID.toString()).arg(entityScript).arg(currentEntityID.toString());
|
||||
#endif
|
||||
_deferredEntityLoads.push_back({ entityID, entityScript });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// the scriptURL slot is available; flag as in-use
|
||||
_occupiedScriptURLs[entityScript] = entityID;
|
||||
|
||||
#ifdef DEBUG_ENTITY_STATES
|
||||
auto previousStatus = _entityScripts.contains(entityID) ? _entityScripts[entityID].status : EntityScriptStatus::PENDING;
|
||||
qCDebug(scriptengine) << "loadEntityScript.LOADING: " << entityScript << entityID.toString()
|
||||
<< "(previous: " << previousStatus << ")";
|
||||
#endif
|
||||
|
||||
EntityScriptDetails newDetails;
|
||||
newDetails.scriptText = entityScript;
|
||||
newDetails.status = EntityScriptStatus::LOADING;
|
||||
newDetails.definingSandboxURL = currentSandboxURL;
|
||||
setEntityScriptDetails(entityID, newDetails);
|
||||
|
||||
auto scriptCache = DependencyManager::get<ScriptCache>();
|
||||
// note: see EntityTreeRenderer.cpp for shared pointer lifecycle management
|
||||
QWeakPointer<ScriptEngine> 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);
|
||||
if (!strongRef) {
|
||||
qCWarning(scriptengine) << "loadEntityScript.contentAvailable -- ScriptEngine was deleted during getScriptContents!!";
|
||||
return;
|
||||
}
|
||||
if (isStopping()) {
|
||||
#ifdef DEBUG_ENTITY_STATES
|
||||
qCDebug(scriptengine) << "loadEntityScript.contentAvailable -- stopping";
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
executeOnScriptThread([=]{
|
||||
#ifdef DEBUG_ENTITY_STATES
|
||||
qCDebug(scriptengine) << "loadEntityScript.contentAvailable" << status << QUrl(url).fileName() << entityID.toString();
|
||||
#endif
|
||||
if (!isStopping() && _entityScripts.contains(entityID)) {
|
||||
entityScriptContentAvailable(entityID, url, contents, isURL, success, status);
|
||||
} else {
|
||||
#ifdef DEBUG_ENTITY_STATES
|
||||
qCDebug(scriptengine) << "loadEntityScript.contentAvailable -- aborting";
|
||||
#endif
|
||||
}
|
||||
// recheck whether us since may have been set to BAD_SCRIPT_UUID_PLACEHOLDER in entityScriptContentAvailable
|
||||
if (_occupiedScriptURLs.contains(entityScript) && _occupiedScriptURLs[entityScript] == entityID) {
|
||||
_occupiedScriptURLs.remove(entityScript);
|
||||
}
|
||||
});
|
||||
}, forceRedownload);
|
||||
}
|
||||
|
||||
// since all of these operations can be asynch we will always do the actual work in the response handler
|
||||
|
@ -1555,25 +1685,51 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co
|
|||
|
||||
auto scriptCache = DependencyManager::get<ScriptCache>();
|
||||
bool isFileUrl = isURL && scriptOrURL.startsWith("file://");
|
||||
auto fileName = isURL ? scriptOrURL : "EmbeddedEntityScript";
|
||||
auto fileName = isURL ? scriptOrURL : "about:EmbeddedEntityScript";
|
||||
|
||||
const EntityScriptDetails &oldDetails = _entityScripts[entityID];
|
||||
const QString entityScript = oldDetails.scriptText;
|
||||
|
||||
EntityScriptDetails newDetails;
|
||||
newDetails.scriptText = scriptOrURL;
|
||||
|
||||
if (!success) {
|
||||
newDetails.status = EntityScriptStatus::ERROR_LOADING_SCRIPT;
|
||||
newDetails.errorInfo = "Failed to load script (" + status + ")";
|
||||
// If an error happens below, we want to update newDetails with the new status info
|
||||
// and also abort any pending Entity loads that are waiting on the exact same script URL.
|
||||
auto setError = [&](const QString &errorInfo, const EntityScriptStatus& status) {
|
||||
newDetails.errorInfo = errorInfo;
|
||||
newDetails.status = status;
|
||||
setEntityScriptDetails(entityID, newDetails);
|
||||
|
||||
#ifdef DEBUG_ENTITY_STATES
|
||||
qCDebug(scriptengine) << "entityScriptContentAvailable -- flagging " << entityScript << " as BAD_SCRIPT_UUID_PLACEHOLDER";
|
||||
#endif
|
||||
// flag the original entityScript as unusuable
|
||||
_occupiedScriptURLs[entityScript] = BAD_SCRIPT_UUID_PLACEHOLDER;
|
||||
processDeferredEntityLoads(entityScript, entityID);
|
||||
};
|
||||
|
||||
// NETWORK / FILESYSTEM ERRORS
|
||||
if (!success) {
|
||||
setError("Failed to load script (" + status + ")", EntityScriptStatus::ERROR_LOADING_SCRIPT);
|
||||
return;
|
||||
}
|
||||
|
||||
// SYNTAX ERRORS
|
||||
auto syntaxError = lintScript(contents, fileName);
|
||||
if (syntaxError.isError()) {
|
||||
auto message = syntaxError.property("formatted").toString();
|
||||
if (message.isEmpty()) {
|
||||
message = syntaxError.toString();
|
||||
}
|
||||
setError(QString("Bad syntax (%1)").arg(message), EntityScriptStatus::ERROR_RUNNING_SCRIPT);
|
||||
syntaxError.setProperty("detail", entityID.toString());
|
||||
emit unhandledException(syntaxError);
|
||||
return;
|
||||
}
|
||||
QScriptProgram program { contents, fileName };
|
||||
if (!syntaxError.isNull() || program.isNull()) {
|
||||
newDetails.status = EntityScriptStatus::ERROR_RUNNING_SCRIPT;
|
||||
newDetails.errorInfo = QString("Bad syntax (%1)").arg(syntaxError);
|
||||
setEntityScriptDetails(entityID, newDetails);
|
||||
qCDebug(scriptengine) << newDetails.errorInfo << scriptOrURL;
|
||||
if (program.isNull()) {
|
||||
setError("Bad program (isNull)", EntityScriptStatus::ERROR_RUNNING_SCRIPT);
|
||||
emit unhandledException(makeError("program.isNull"));
|
||||
return; // done processing script
|
||||
}
|
||||
|
||||
|
@ -1581,10 +1737,11 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co
|
|||
setParentURL(scriptOrURL);
|
||||
}
|
||||
|
||||
// SANITY/PERFORMANCE CHECK USING SANDBOX
|
||||
const int SANDBOX_TIMEOUT = 0.25 * MSECS_PER_SECOND;
|
||||
BaseScriptEngine sandbox;
|
||||
sandbox.setProcessEventsInterval(SANDBOX_TIMEOUT);
|
||||
QScriptValue testConstructor;
|
||||
QScriptValue testConstructor, exception;
|
||||
{
|
||||
QTimer timeout;
|
||||
timeout.setSingleShot(true);
|
||||
|
@ -1598,18 +1755,26 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co
|
|||
context->throwError(QString("Timed out (entity constructors are limited to %1ms)").arg(SANDBOX_TIMEOUT));
|
||||
}
|
||||
});
|
||||
|
||||
testConstructor = sandbox.evaluate(program);
|
||||
|
||||
if (sandbox.hasUncaughtException()) {
|
||||
exception = sandbox.cloneUncaughtException(QString("(preflight %1)").arg(entityID.toString()));
|
||||
sandbox.clearExceptions();
|
||||
} else if (testConstructor.isError()) {
|
||||
exception = testConstructor;
|
||||
}
|
||||
}
|
||||
|
||||
QString exceptionMessage = sandbox.formatUncaughtException(program.fileName());
|
||||
if (!exceptionMessage.isNull()) {
|
||||
newDetails.status = EntityScriptStatus::ERROR_RUNNING_SCRIPT;
|
||||
newDetails.errorInfo = exceptionMessage;
|
||||
setEntityScriptDetails(entityID, newDetails);
|
||||
qCDebug(scriptengine) << "----- ScriptEngine::entityScriptContentAvailable -- hadUncaughtExceptions (" << scriptOrURL << ")";
|
||||
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);
|
||||
emit unhandledException(exception);
|
||||
return;
|
||||
}
|
||||
|
||||
// CONSTRUCTOR VIABILITY
|
||||
if (!testConstructor.isFunction()) {
|
||||
QString testConstructorType = QString(testConstructor.toVariant().typeName());
|
||||
if (testConstructorType == "") {
|
||||
|
@ -1620,32 +1785,48 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co
|
|||
if (testConstructorValue.size() > maxTestConstructorValueSize) {
|
||||
testConstructorValue = testConstructorValue.mid(0, maxTestConstructorValueSize) + "...";
|
||||
}
|
||||
scriptErrorMessage("Error -- ScriptEngine::loadEntityScript() entity:" + entityID.toString()
|
||||
+ "failed to load entity script -- expected a function, got " + testConstructorType
|
||||
+ "," + testConstructorValue
|
||||
+ "," + scriptOrURL);
|
||||
auto message = QString("failed to load entity script -- expected a function, got %1, %2")
|
||||
.arg(testConstructorType).arg(testConstructorValue);
|
||||
|
||||
newDetails.status = EntityScriptStatus::ERROR_RUNNING_SCRIPT;
|
||||
newDetails.errorInfo = "Could not find constructor";
|
||||
setEntityScriptDetails(entityID, newDetails);
|
||||
auto err = makeError(message);
|
||||
err.setProperty("fileName", scriptOrURL);
|
||||
err.setProperty("detail", "(constructor " + entityID.toString() + ")");
|
||||
|
||||
qCDebug(scriptengine) << "----- ScriptEngine::entityScriptContentAvailable -- failed to run (" << scriptOrURL << ")";
|
||||
setError("Could not find constructor (" + testConstructorType + ")", EntityScriptStatus::ERROR_RUNNING_SCRIPT);
|
||||
emit unhandledException(err);
|
||||
return; // done processing script
|
||||
}
|
||||
|
||||
// (this feeds into refreshFileScript)
|
||||
int64_t lastModified = 0;
|
||||
if (isFileUrl) {
|
||||
QString file = QUrl(scriptOrURL).toLocalFile();
|
||||
lastModified = (quint64)QFileInfo(file).lastModified().toMSecsSinceEpoch();
|
||||
}
|
||||
|
||||
// THE ACTUAL EVALUATION AND CONSTRUCTION
|
||||
QScriptValue entityScriptConstructor, entityScriptObject;
|
||||
QUrl sandboxURL = currentSandboxURL.isEmpty() ? scriptOrURL : currentSandboxURL;
|
||||
auto initialization = [&]{
|
||||
entityScriptConstructor = evaluate(contents, fileName);
|
||||
entityScriptObject = entityScriptConstructor.construct();
|
||||
|
||||
if (hasUncaughtException()) {
|
||||
entityScriptObject = cloneUncaughtException("(construct " + entityID.toString() + ")");
|
||||
clearExceptions();
|
||||
}
|
||||
};
|
||||
|
||||
doWithEnvironment(entityID, sandboxURL, initialization);
|
||||
|
||||
if (entityScriptObject.isError()) {
|
||||
auto exception = entityScriptObject;
|
||||
setError(formatException(exception), EntityScriptStatus::ERROR_RUNNING_SCRIPT);
|
||||
emit unhandledException(exception);
|
||||
return;
|
||||
}
|
||||
|
||||
// ... AND WE HAVE LIFTOFF
|
||||
newDetails.status = EntityScriptStatus::RUNNING;
|
||||
newDetails.scriptObject = entityScriptObject;
|
||||
newDetails.lastModified = lastModified;
|
||||
|
@ -1658,6 +1839,9 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co
|
|||
|
||||
// if we got this far, then call the preload method
|
||||
callEntityScriptMethod(entityID, "preload");
|
||||
|
||||
_occupiedScriptURLs.remove(entityScript);
|
||||
processDeferredEntityLoads(entityScript, entityID);
|
||||
}
|
||||
|
||||
void ScriptEngine::unloadEntityScript(const EntityItemID& entityID) {
|
||||
|
@ -1677,13 +1861,25 @@ void ScriptEngine::unloadEntityScript(const EntityItemID& entityID) {
|
|||
#endif
|
||||
|
||||
if (_entityScripts.contains(entityID)) {
|
||||
const EntityScriptDetails &oldDetails = _entityScripts[entityID];
|
||||
if (isEntityScriptRunning(entityID)) {
|
||||
callEntityScriptMethod(entityID, "unload");
|
||||
} else {
|
||||
qCDebug(scriptengine) << "unload called while !running" << entityID << oldDetails.status;
|
||||
}
|
||||
if (oldDetails.status != EntityScriptStatus::UNLOADED) {
|
||||
EntityScriptDetails newDetails;
|
||||
newDetails.status = EntityScriptStatus::UNLOADED;
|
||||
newDetails.lastModified = QDateTime::currentMSecsSinceEpoch();
|
||||
// keep scriptText populated for the current need to "debouce" duplicate calls to unloadEntityScript
|
||||
newDetails.scriptText = oldDetails.scriptText;
|
||||
setEntityScriptDetails(entityID, newDetails);
|
||||
}
|
||||
EntityScriptDetails newDetails;
|
||||
newDetails.status = EntityScriptStatus::UNLOADED;
|
||||
setEntityScriptDetails(entityID, newDetails);
|
||||
stopAllTimersForEntityScript(entityID);
|
||||
{
|
||||
// FIXME: shouldn't have to do this here, but currently something seems to be firing unloads moments after firing initial load requests
|
||||
processDeferredEntityLoads(oldDetails.scriptText, entityID);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1704,15 +1900,14 @@ void ScriptEngine::unloadAllEntityScripts() {
|
|||
}
|
||||
_entityScripts.clear();
|
||||
emit entityScriptDetailsUpdated();
|
||||
_occupiedScriptURLs.clear();
|
||||
|
||||
#ifdef DEBUG_ENGINE_STATE
|
||||
qCDebug(scriptengine) << "---- CURRENT STATE OF ENGINE: --------------------------";
|
||||
QScriptValueIterator it(globalObject());
|
||||
while (it.hasNext()) {
|
||||
it.next();
|
||||
qCDebug(scriptengine) << it.name() << ":" << it.value().toString();
|
||||
}
|
||||
qCDebug(scriptengine) << "--------------------------------------------------------";
|
||||
_debugDump(
|
||||
"---- CURRENT STATE OF ENGINE: --------------------------",
|
||||
globalObject(),
|
||||
"--------------------------------------------------------"
|
||||
);
|
||||
#endif // DEBUG_ENGINE_STATE
|
||||
}
|
||||
|
||||
|
@ -1734,17 +1929,7 @@ void ScriptEngine::refreshFileScript(const EntityItemID& entityID) {
|
|||
auto lastModified = QFileInfo(filePath).lastModified().toMSecsSinceEpoch();
|
||||
if (lastModified > details.lastModified) {
|
||||
scriptInfoMessage("Reloading modified script " + details.scriptText);
|
||||
|
||||
QFile file(filePath);
|
||||
file.open(QIODevice::ReadOnly);
|
||||
QString scriptContents = QTextStream(&file).readAll();
|
||||
this->unloadEntityScript(entityID);
|
||||
this->entityScriptContentAvailable(entityID, details.scriptText, scriptContents, true, true, "Success");
|
||||
if (!isEntityScriptRunning(entityID)) {
|
||||
scriptWarningMessage("Reload script " + details.scriptText + " failed");
|
||||
} else {
|
||||
details = _entityScripts[entityID];
|
||||
}
|
||||
loadEntityScript(entityID, details.scriptText, true);
|
||||
}
|
||||
}
|
||||
recurseGuard = false;
|
||||
|
@ -1768,14 +1953,14 @@ void ScriptEngine::doWithEnvironment(const EntityItemID& entityID, const QUrl& s
|
|||
#else
|
||||
operation();
|
||||
#endif
|
||||
if (hasUncaughtException()) {
|
||||
reportUncaughtException();
|
||||
if (!isEvaluating() && hasUncaughtException()) {
|
||||
emit unhandledException(cloneUncaughtException(!entityID.isNull() ? entityID.toString() : __FUNCTION__));
|
||||
clearExceptions();
|
||||
}
|
||||
|
||||
currentEntityIdentifier = oldIdentifier;
|
||||
currentSandboxURL = oldSandboxURL;
|
||||
}
|
||||
|
||||
void ScriptEngine::callWithEnvironment(const EntityItemID& entityID, const QUrl& sandboxURL, QScriptValue function, QScriptValue thisObject, QScriptValueList args) {
|
||||
auto operation = [&]() {
|
||||
function.call(thisObject, args);
|
||||
|
@ -1850,7 +2035,6 @@ void ScriptEngine::callEntityScriptMethod(const EntityItemID& entityID, const QS
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
void ScriptEngine::callEntityScriptMethod(const EntityItemID& entityID, const QString& methodName, const EntityItemID& otherID, const Collision& collision) {
|
||||
if (QThread::currentThread() != thread()) {
|
||||
#ifdef THREAD_DEBUGGING
|
||||
|
@ -1885,3 +2069,4 @@ void ScriptEngine::callEntityScriptMethod(const EntityItemID& entityID, const QS
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -35,6 +35,7 @@
|
|||
#include "ArrayBufferClass.h"
|
||||
#include "AssetScriptingInterface.h"
|
||||
#include "AudioScriptingInterface.h"
|
||||
#include "BaseScriptEngine.h"
|
||||
#include "Quat.h"
|
||||
#include "Mat4.h"
|
||||
#include "ScriptCache.h"
|
||||
|
@ -54,6 +55,13 @@ public:
|
|||
QUrl definingSandboxURL;
|
||||
};
|
||||
|
||||
class DeferredLoadEntity {
|
||||
public:
|
||||
EntityItemID entityID;
|
||||
QString entityScript;
|
||||
//bool forceRedownload;
|
||||
};
|
||||
|
||||
typedef QList<CallbackData> CallbackList;
|
||||
typedef QHash<QString, CallbackList> RegisteredEventHandlers;
|
||||
|
||||
|
@ -67,18 +75,10 @@ public:
|
|||
QString scriptText { "" };
|
||||
QScriptValue scriptObject { QScriptValue() };
|
||||
int64_t lastModified { 0 };
|
||||
QUrl definingSandboxURL { QUrl() };
|
||||
QUrl definingSandboxURL { QUrl("about:EntityScript") };
|
||||
};
|
||||
|
||||
// common base class with just QScriptEngine-dependent helper methods
|
||||
class BaseScriptEngine : public QScriptEngine {
|
||||
public:
|
||||
static const QString SCRIPT_EXCEPTION_FORMAT;
|
||||
QString lintScript(const QString& sourceCode, const QString& fileName, const int lineNumber = 1);
|
||||
QString formatUncaughtException(const QString& overrideFileName = QString());
|
||||
};
|
||||
|
||||
class ScriptEngine : public BaseScriptEngine, public EntitiesScriptEngineProvider {
|
||||
class ScriptEngine : public BaseScriptEngine, public EntitiesScriptEngineProvider, public QEnableSharedFromThis<ScriptEngine> {
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(QString context READ getContext)
|
||||
public:
|
||||
|
@ -91,14 +91,13 @@ public:
|
|||
};
|
||||
|
||||
static int processLevelMaxRetries;
|
||||
ScriptEngine(Context context, const QString& scriptContents = NO_SCRIPT, const QString& fileNameString = QString(""));
|
||||
ScriptEngine(Context context, const QString& scriptContents = NO_SCRIPT, const QString& fileNameString = QString("about:ScriptEngine"));
|
||||
~ScriptEngine();
|
||||
|
||||
/// run the script in a dedicated thread. This will have the side effect of evalulating
|
||||
/// the current script contents and calling run(). Callers will likely want to register the script with external
|
||||
/// services before calling this.
|
||||
void runInThread();
|
||||
Q_INVOKABLE void executeOnScriptThread(std::function<void()> function, bool blocking = false);
|
||||
|
||||
void runDebuggable();
|
||||
|
||||
|
@ -162,16 +161,16 @@ public:
|
|||
Q_INVOKABLE QObject* setTimeout(const QScriptValue& function, int timeoutMS);
|
||||
Q_INVOKABLE void clearInterval(QObject* timer) { stopTimer(reinterpret_cast<QTimer*>(timer)); }
|
||||
Q_INVOKABLE void clearTimeout(QObject* timer) { stopTimer(reinterpret_cast<QTimer*>(timer)); }
|
||||
|
||||
Q_INVOKABLE void print(const QString& message);
|
||||
Q_INVOKABLE QUrl resolvePath(const QString& path) const;
|
||||
Q_INVOKABLE QUrl resourcesPath() const;
|
||||
|
||||
// Entity Script Related methods
|
||||
Q_INVOKABLE QString getEntityScriptStatus(const EntityItemID& entityID);
|
||||
Q_INVOKABLE bool isEntityScriptRunning(const EntityItemID& entityID) {
|
||||
return _entityScripts.contains(entityID) && _entityScripts[entityID].status == EntityScriptStatus::RUNNING;
|
||||
}
|
||||
static void loadEntityScript(QWeakPointer<ScriptEngine> theEngine, const EntityItemID& entityID, const QString& entityScript, bool forceRedownload);
|
||||
Q_INVOKABLE void loadEntityScript(const EntityItemID& entityID, const QString& entityScript, bool forceRedownload);
|
||||
Q_INVOKABLE void unloadEntityScript(const EntityItemID& entityID); // will call unload method
|
||||
Q_INVOKABLE void unloadAllEntityScripts();
|
||||
Q_INVOKABLE void callEntityScriptMethod(const EntityItemID& entityID, const QString& methodName,
|
||||
|
@ -212,7 +211,6 @@ public:
|
|||
bool getEntityScriptDetails(const EntityItemID& entityID, EntityScriptDetails &details) const;
|
||||
|
||||
public slots:
|
||||
int evaluatePending() const { return _evaluatesPending; }
|
||||
void callAnimationStateHandler(QScriptValue callback, AnimVariantMap parameters, QStringList names, bool useNames, AnimVariantResultHandler resultHandler);
|
||||
void updateMemoryCost(const qint64&);
|
||||
|
||||
|
@ -228,7 +226,6 @@ signals:
|
|||
void warningMessage(const QString& message);
|
||||
void infoMessage(const QString& message);
|
||||
void runningStateChanged();
|
||||
void evaluationFinished(QScriptValue result, bool isException);
|
||||
void loadScript(const QString& scriptName, bool isUserLoaded);
|
||||
void reloadScript(const QString& scriptName, bool isUserLoaded);
|
||||
void doneRunning();
|
||||
|
@ -239,8 +236,9 @@ signals:
|
|||
|
||||
protected:
|
||||
void init();
|
||||
Q_INVOKABLE void executeOnScriptThread(std::function<void()> function, const Qt::ConnectionType& type = Qt::QueuedConnection );
|
||||
|
||||
QString reportUncaughtException(const QString& overrideFileName = QString());
|
||||
QString logException(const QScriptValue& exception);
|
||||
void timerFired();
|
||||
void stopAllTimers();
|
||||
void stopAllTimersForEntityScript(const EntityItemID& entityID);
|
||||
|
@ -248,6 +246,7 @@ protected:
|
|||
void updateEntityScriptStatus(const EntityItemID& entityID, const EntityScriptStatus& status, const QString& errorInfo = QString());
|
||||
void setEntityScriptDetails(const EntityItemID& entityID, const EntityScriptDetails& details);
|
||||
void setParentURL(const QString& parentURL) { _parentURL = parentURL; }
|
||||
void processDeferredEntityLoads(const QString& entityScript, const EntityItemID& leaderID);
|
||||
|
||||
QObject* setupTimerWithInterval(const QScriptValue& function, int intervalMS, bool isSingleShot);
|
||||
void stopTimer(QTimer* timer);
|
||||
|
@ -262,17 +261,18 @@ protected:
|
|||
void callWithEnvironment(const EntityItemID& entityID, const QUrl& sandboxURL, QScriptValue function, QScriptValue thisObject, QScriptValueList args);
|
||||
|
||||
Context _context;
|
||||
|
||||
QString _scriptContents;
|
||||
QString _parentURL;
|
||||
std::atomic<bool> _isFinished { false };
|
||||
std::atomic<bool> _isRunning { false };
|
||||
std::atomic<bool> _isStopping { false };
|
||||
int _evaluatesPending { 0 };
|
||||
bool _isInitialized { false };
|
||||
QHash<QTimer*, CallbackData> _timerFunctionMap;
|
||||
QSet<QUrl> _includedURLs;
|
||||
QHash<EntityItemID, EntityScriptDetails> _entityScripts;
|
||||
QHash<QString, EntityItemID> _occupiedScriptURLs;
|
||||
QList<DeferredLoadEntity> _deferredEntityLoads;
|
||||
|
||||
bool _isThreaded { false };
|
||||
QScriptEngineDebugger* _debugger { nullptr };
|
||||
bool _debuggable { false };
|
||||
|
|
|
@ -364,25 +364,43 @@ QStringList ScriptEngines::getRunningScripts() {
|
|||
}
|
||||
|
||||
void ScriptEngines::stopAllScripts(bool restart) {
|
||||
QVector<QString> toReload;
|
||||
QReadLocker lock(&_scriptEnginesHashLock);
|
||||
for (QHash<QUrl, ScriptEngine*>::const_iterator it = _scriptEnginesHash.constBegin();
|
||||
it != _scriptEnginesHash.constEnd(); it++) {
|
||||
ScriptEngine *scriptEngine = it.value();
|
||||
// skip already stopped scripts
|
||||
if (it.value()->isFinished() || it.value()->isStopping()) {
|
||||
if (scriptEngine->isFinished() || scriptEngine->isStopping()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// queue user scripts if restarting
|
||||
if (restart && it.value()->isUserLoaded()) {
|
||||
connect(it.value(), &ScriptEngine::finished, this, [this](QString scriptName, ScriptEngine* engine) {
|
||||
reloadScript(scriptName);
|
||||
});
|
||||
if (restart && scriptEngine->isUserLoaded()) {
|
||||
toReload << it.key().toString();
|
||||
}
|
||||
|
||||
// stop all scripts
|
||||
it.value()->stop(true);
|
||||
qCDebug(scriptengine) << "stopping script..." << it.key();
|
||||
scriptEngine->stop();
|
||||
}
|
||||
// wait for engines to stop (ie: providing time for .scriptEnding cleanup handlers to run) before
|
||||
// triggering reload of any Client scripts / Entity scripts
|
||||
QTimer::singleShot(500, this, [=]() {
|
||||
for(const auto &scriptName : toReload) {
|
||||
auto scriptEngine = getScriptEngine(scriptName);
|
||||
if (scriptEngine && !scriptEngine->isFinished()) {
|
||||
qCDebug(scriptengine) << "waiting on script:" << scriptName;
|
||||
scriptEngine->waitTillDoneRunning();
|
||||
qCDebug(scriptengine) << "done waiting on script:" << scriptName;
|
||||
}
|
||||
qCDebug(scriptengine) << "reloading script..." << scriptName;
|
||||
reloadScript(scriptName);
|
||||
}
|
||||
if (restart) {
|
||||
qCDebug(scriptengine) << "stopAllScripts -- emitting scriptsReloading";
|
||||
emit scriptsReloading();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bool ScriptEngines::stopScript(const QString& rawScriptURL, bool restart) {
|
||||
|
@ -421,9 +439,10 @@ void ScriptEngines::setScriptsLocation(const QString& scriptsLocation) {
|
|||
}
|
||||
|
||||
void ScriptEngines::reloadAllScripts() {
|
||||
qCDebug(scriptengine) << "reloadAllScripts -- clearing caches";
|
||||
DependencyManager::get<ScriptCache>()->clearCache();
|
||||
DependencyManager::get<OffscreenUi>()->clearCache();
|
||||
emit scriptsReloading();
|
||||
qCDebug(scriptengine) << "reloadAllScripts -- stopping all scripts";
|
||||
stopAllScripts(true);
|
||||
}
|
||||
|
||||
|
@ -456,7 +475,7 @@ ScriptEngine* ScriptEngines::loadScript(const QUrl& scriptFilename, bool isUserL
|
|||
return scriptEngine;
|
||||
}
|
||||
|
||||
scriptEngine = new ScriptEngine(_context, NO_SCRIPT, "");
|
||||
scriptEngine = new ScriptEngine(_context, NO_SCRIPT, "about:" + scriptFilename.fileName());
|
||||
scriptEngine->setUserLoaded(isUserLoaded);
|
||||
connect(scriptEngine, &ScriptEngine::doneRunning, this, [scriptEngine] {
|
||||
scriptEngine->deleteLater();
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
#include <QUrl>
|
||||
#include "PathUtils.h"
|
||||
#include <QtCore/QStandardPaths>
|
||||
|
||||
#include <mutex> // std::once
|
||||
|
||||
const QString& PathUtils::resourcesPath() {
|
||||
#ifdef Q_OS_MAC
|
||||
|
@ -82,3 +82,28 @@ QUrl defaultScriptsLocation() {
|
|||
QFileInfo fileInfo(path);
|
||||
return QUrl::fromLocalFile(fileInfo.canonicalFilePath());
|
||||
}
|
||||
|
||||
|
||||
QString PathUtils::stripFilename(const QUrl& url) {
|
||||
// Guard against meaningless query and fragment parts.
|
||||
// Do NOT use PreferLocalFile as its behavior is unpredictable (e.g., on defaultScriptsLocation())
|
||||
return url.toString(QUrl::RemoveFilename | QUrl::RemoveQuery | QUrl::RemoveFragment);
|
||||
}
|
||||
|
||||
Qt::CaseSensitivity PathUtils::getFSCaseSensitivity() {
|
||||
static Qt::CaseSensitivity sensitivity { Qt::CaseSensitive };
|
||||
static std::once_flag once;
|
||||
std::call_once(once, [&] {
|
||||
QString path = defaultScriptsLocation().toLocalFile();
|
||||
QFileInfo upperFI(path.toUpper());
|
||||
QFileInfo lowerFI(path.toLower());
|
||||
sensitivity = (upperFI == lowerFI) ? Qt::CaseInsensitive : Qt::CaseSensitive;
|
||||
});
|
||||
return sensitivity;
|
||||
}
|
||||
|
||||
bool PathUtils::isDescendantOf(const QUrl& childURL, const QUrl& parentURL) {
|
||||
QString child = stripFilename(childURL);
|
||||
QString parent = stripFilename(parentURL);
|
||||
return child.startsWith(parent, PathUtils::getFSCaseSensitivity());
|
||||
}
|
||||
|
|
|
@ -28,6 +28,11 @@ class PathUtils : public QObject, public Dependency {
|
|||
public:
|
||||
static const QString& resourcesPath();
|
||||
static QString getRootDataDirectory();
|
||||
|
||||
static Qt::CaseSensitivity getFSCaseSensitivity();
|
||||
static QString stripFilename(const QUrl& url);
|
||||
// note: this is FS-case-sensitive version of parentURL.isParentOf(childURL)
|
||||
static bool isDescendantOf(const QUrl& childURL, const QUrl& parentURL);
|
||||
};
|
||||
|
||||
QString fileNameWithoutExtension(const QString& fileName, const QVector<QString> possibleExtensions);
|
||||
|
|
21
scripts/developer/tests/entityServerStampedeTest-entity.js
Normal file
21
scripts/developer/tests/entityServerStampedeTest-entity.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
(function() {
|
||||
return {
|
||||
preload: function(uuid) {
|
||||
var props = Entities.getEntityProperties(uuid);
|
||||
var shape = props.shape === 'Sphere' ? 'Hexagon' : 'Sphere';
|
||||
|
||||
Entities.editEntity(uuid, {
|
||||
shape: shape,
|
||||
color: { red: 0xff, green: 0xff, blue: 0xff },
|
||||
});
|
||||
this.name = props.name;
|
||||
print("preload", this.name);
|
||||
},
|
||||
unload: function(uuid) {
|
||||
print("unload", this.name);
|
||||
Entities.editEntity(uuid, {
|
||||
color: { red: 0x0f, green: 0x0f, blue: 0xff },
|
||||
});
|
||||
},
|
||||
};
|
||||
})
|
33
scripts/developer/tests/entityServerStampedeTest.js
Normal file
33
scripts/developer/tests/entityServerStampedeTest.js
Normal file
|
@ -0,0 +1,33 @@
|
|||
var NUM_ENTITIES = 100;
|
||||
var RADIUS = 2;
|
||||
var DIV = NUM_ENTITIES / Math.PI / 2;
|
||||
var PASS_SCRIPT_URL = Script.resolvePath('entityServerStampedeTest-entity.js');
|
||||
var FAIL_SCRIPT_URL = Script.resolvePath('entityStampedeTest-entity-fail.js');
|
||||
|
||||
var origin = Vec3.sum(MyAvatar.position, Vec3.multiply(5, Quat.getFront(MyAvatar.orientation)));
|
||||
origin.y += HMD.eyeHeight;
|
||||
|
||||
var uuids = [];
|
||||
|
||||
Script.scriptEnding.connect(function() {
|
||||
uuids.forEach(function(id) {
|
||||
Entities.deleteEntity(id);
|
||||
});
|
||||
});
|
||||
|
||||
for (var i=0; i < NUM_ENTITIES; i++) {
|
||||
var failGroup = i % 2;
|
||||
uuids.push(Entities.addEntity({
|
||||
type: 'Shape',
|
||||
shape: failGroup ? 'Sphere' : 'Icosahedron',
|
||||
name: 'SERVER-entityStampedeTest-' + i,
|
||||
lifetime: 120,
|
||||
position: Vec3.sum(origin, Vec3.multiplyQbyV(
|
||||
MyAvatar.orientation, { x: Math.sin(i / DIV) * RADIUS, y: Math.cos(i / DIV) * RADIUS, z: 0 }
|
||||
)),
|
||||
serverScripts: (failGroup ? FAIL_SCRIPT_URL : PASS_SCRIPT_URL) + Settings.getValue('cache_buster'),
|
||||
dimensions: Vec3.HALF,
|
||||
color: { red: 0, green: 0, blue: 0 },
|
||||
}, !Entities.serversExist()));
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
(function() {
|
||||
throw new Error(Script.resolvePath(''));
|
||||
})
|
21
scripts/developer/tests/entityStampedeTest-entity.js
Normal file
21
scripts/developer/tests/entityStampedeTest-entity.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
(function() {
|
||||
return {
|
||||
preload: function(uuid) {
|
||||
var props = Entities.getEntityProperties(uuid);
|
||||
var shape = props.shape === 'Sphere' ? 'Cube' : 'Sphere';
|
||||
|
||||
Entities.editEntity(uuid, {
|
||||
shape: shape,
|
||||
color: { red: 0xff, green: 0xff, blue: 0xff },
|
||||
});
|
||||
this.name = props.name;
|
||||
print("preload", this.name);
|
||||
},
|
||||
unload: function(uuid) {
|
||||
print("unload", this.name);
|
||||
Entities.editEntity(uuid, {
|
||||
color: { red: 0xff, green: 0x0f, blue: 0x0f },
|
||||
});
|
||||
},
|
||||
};
|
||||
})
|
32
scripts/developer/tests/entityStampedeTest.js
Normal file
32
scripts/developer/tests/entityStampedeTest.js
Normal file
|
@ -0,0 +1,32 @@
|
|||
var NUM_ENTITIES = 100;
|
||||
var RADIUS = 2;
|
||||
var DIV = NUM_ENTITIES / Math.PI / 2;
|
||||
var PASS_SCRIPT_URL = Script.resolvePath('').replace('.js', '-entity.js');
|
||||
var FAIL_SCRIPT_URL = Script.resolvePath('').replace('.js', '-entity-fail.js');
|
||||
|
||||
var origin = Vec3.sum(MyAvatar.position, Vec3.multiply(5, Quat.getFront(MyAvatar.orientation)));
|
||||
origin.y += HMD.eyeHeight;
|
||||
|
||||
var uuids = [];
|
||||
|
||||
Script.scriptEnding.connect(function() {
|
||||
uuids.forEach(function(id) {
|
||||
Entities.deleteEntity(id);
|
||||
});
|
||||
});
|
||||
|
||||
for (var i=0; i < NUM_ENTITIES; i++) {
|
||||
var failGroup = i % 2;
|
||||
uuids.push(Entities.addEntity({
|
||||
type: 'Shape',
|
||||
shape: failGroup ? 'Sphere' : 'Icosahedron',
|
||||
name: 'entityStampedeTest-' + i,
|
||||
lifetime: 120,
|
||||
position: Vec3.sum(origin, Vec3.multiplyQbyV(
|
||||
MyAvatar.orientation, { x: Math.sin(i / DIV) * RADIUS, y: Math.cos(i / DIV) * RADIUS, z: 0 }
|
||||
)),
|
||||
script: (failGroup ? FAIL_SCRIPT_URL : PASS_SCRIPT_URL) + Settings.getValue('cache_buster'),
|
||||
dimensions: Vec3.HALF,
|
||||
color: { red: 0, green: 0, blue: 0 },
|
||||
}, !Entities.serversExist()));
|
||||
}
|
6
scripts/developer/tests/unit_tests/scriptTests/error.js
Normal file
6
scripts/developer/tests/unit_tests/scriptTests/error.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
afterError = false;
|
||||
throw new Error('error.js');
|
||||
afterError = true;
|
||||
|
||||
(1,eval)('this').$finishes.push(Script.resolvePath(''));
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
afterError = false;
|
||||
outer = null;
|
||||
Script.include('./nested/error.js?' + Settings.getValue('cache_buster'));
|
||||
outer = {
|
||||
inner: inner.lib,
|
||||
sibling: sibling.lib,
|
||||
};
|
||||
afterError = true;
|
||||
|
||||
(1,eval)("this").$finishes.push(Script.resolvePath(''));
|
|
@ -0,0 +1,10 @@
|
|||
afterError = false;
|
||||
outer = null;
|
||||
Script.include('./nested/syntax-error.js?' + Settings.getValue('cache_buster'));
|
||||
outer = {
|
||||
inner: inner.lib,
|
||||
sibling: sibling.lib,
|
||||
};
|
||||
afterError = true;
|
||||
|
||||
(1,eval)("this").$finishes.push(Script.resolvePath(''));
|
|
@ -0,0 +1,5 @@
|
|||
afterError = false;
|
||||
throw new Error('nested/error.js');
|
||||
afterError = true;
|
||||
|
||||
(1,eval)("this").$finishes.push(Script.resolvePath(''));
|
|
@ -0,0 +1,5 @@
|
|||
Script.include('sibling.js');
|
||||
inner = {
|
||||
lib: "nested/lib.js",
|
||||
};
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
sibling = {
|
||||
lib: "nested/sibling",
|
||||
};
|
|
@ -0,0 +1,3 @@
|
|||
function() {
|
||||
// intentional SyntaxError...
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
afterError = false;
|
||||
outer = null;
|
||||
Script.include('./nested/lib.js');
|
||||
Undefined_symbol;
|
||||
outer = {
|
||||
inner: inner.lib,
|
||||
sibling: sibling.lib,
|
||||
};
|
||||
afterError = true;
|
||||
|
||||
(1,eval)("this").$finishes.push(Script.resolvePath(''));
|
|
@ -0,0 +1,5 @@
|
|||
Script.include('./nested/lib.js');
|
||||
outer = {
|
||||
inner: inner.lib,
|
||||
sibling: sibling.lib,
|
||||
};
|
125
scripts/developer/tests/unit_tests/scriptUnitTests.js
Normal file
125
scripts/developer/tests/unit_tests/scriptUnitTests.js
Normal file
|
@ -0,0 +1,125 @@
|
|||
/* eslint-env jasmine */
|
||||
|
||||
instrument_testrunner();
|
||||
|
||||
describe('Script', function () {
|
||||
// get the current filename without calling Script.resolvePath('')
|
||||
try {
|
||||
throw new Error('stack');
|
||||
} catch(e) {
|
||||
var filename = e.fileName;
|
||||
var dirname = filename.split('/').slice(0, -1).join('/') + '/';
|
||||
var parentdir = dirname.split('/').slice(0, -2).join('/') + '/';
|
||||
}
|
||||
|
||||
// characterization tests
|
||||
// initially these are just to capture how the app works currently
|
||||
var testCases = {
|
||||
'': filename,
|
||||
'.': dirname,
|
||||
'..': parentdir,
|
||||
'about:Entities 1': '',
|
||||
'Entities 1': dirname + 'Entities 1',
|
||||
'./file.js': dirname + 'file.js',
|
||||
'c:/temp/': 'file:///c:/temp/',
|
||||
'c:/temp': 'file:///c:/temp',
|
||||
'/temp/': 'file:///temp/',
|
||||
'c:/': 'file:///c:/',
|
||||
'c:': 'file:///c:',
|
||||
'file:///~/libraries/a.js': 'file:///~/libraries/a.js',
|
||||
'/temp/tested/../file.js': 'file:///temp/tested/../file.js',
|
||||
'/~/libraries/utils.js': 'file:///~/libraries/utils.js',
|
||||
'/temp/file.js': 'file:///temp/file.js',
|
||||
'/~/': 'file:///~/',
|
||||
};
|
||||
describe('resolvePath', function () {
|
||||
Object.keys(testCases).forEach(function(input) {
|
||||
it('(' + JSON.stringify(input) + ')', function () {
|
||||
expect(Script.resolvePath(input)).toEqual(testCases[input]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('include', function () {
|
||||
var old_cache_buster;
|
||||
var cache_buster = '#' + +new Date;
|
||||
beforeAll(function() {
|
||||
old_cache_buster = Settings.getValue('cache_buster');
|
||||
Settings.setValue('cache_buster', cache_buster);
|
||||
});
|
||||
afterAll(function() {
|
||||
Settings.setValue('cache_buster', old_cache_buster);
|
||||
});
|
||||
beforeEach(function() {
|
||||
vec3toStr = undefined;
|
||||
});
|
||||
it('file:///~/system/libraries/utils.js' + cache_buster, function() {
|
||||
Script.include('file:///~/system/libraries/utils.js' + cache_buster);
|
||||
expect(vec3toStr).toEqual(jasmine.any(Function));
|
||||
});
|
||||
it('nested' + cache_buster, function() {
|
||||
Script.include('./scriptTests/top-level.js' + cache_buster);
|
||||
expect(outer).toEqual(jasmine.any(Object));
|
||||
expect(inner).toEqual(jasmine.any(Object));
|
||||
expect(sibling).toEqual(jasmine.any(Object));
|
||||
expect(outer.inner).toEqual(inner.lib);
|
||||
expect(outer.sibling).toEqual(sibling.lib);
|
||||
});
|
||||
describe('errors' + cache_buster, function() {
|
||||
var finishes, oldFinishes;
|
||||
beforeAll(function() {
|
||||
oldFinishes = (1,eval)('this').$finishes;
|
||||
});
|
||||
afterAll(function() {
|
||||
(1,eval)('this').$finishes = oldFinishes;
|
||||
});
|
||||
beforeEach(function() {
|
||||
finishes = (1,eval)('this').$finishes = [];
|
||||
});
|
||||
it('error', function() {
|
||||
// a thrown Error in top-level include aborts that include, but does not throw the error back to JS
|
||||
expect(function() {
|
||||
Script.include('./scriptTests/error.js' + cache_buster);
|
||||
}).not.toThrowError("error.js");
|
||||
expect(finishes.length).toBe(0);
|
||||
});
|
||||
it('top-level-error', function() {
|
||||
// an organice Error in a top-level include aborts that include, but does not throw the error
|
||||
expect(function() {
|
||||
Script.include('./scriptTests/top-level-error.js' + cache_buster);
|
||||
}).not.toThrowError(/Undefined_symbol/);
|
||||
expect(finishes.length).toBe(0);
|
||||
});
|
||||
it('nested', function() {
|
||||
// a thrown Error in a nested include aborts the nested include, but does not abort the top-level script
|
||||
expect(function() {
|
||||
Script.include('./scriptTests/nested-error.js' + cache_buster);
|
||||
}).not.toThrowError("nested/error.js");
|
||||
expect(finishes.length).toBe(1);
|
||||
});
|
||||
it('nested-syntax-error', function() {
|
||||
// a SyntaxError in a nested include breaks only that include (the main script should finish unimpeded)
|
||||
expect(function() {
|
||||
Script.include('./scriptTests/nested-syntax-error.js' + cache_buster);
|
||||
}).not.toThrowError(/SyntaxEror/);
|
||||
expect(finishes.length).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// enable scriptUnitTests to be loaded directly
|
||||
function run() {}
|
||||
function instrument_testrunner() {
|
||||
if (typeof describe === 'undefined') {
|
||||
print('instrumenting jasmine', Script.resolvePath(''));
|
||||
Script.include('../../libraries/jasmine/jasmine.js');
|
||||
Script.include('../../libraries/jasmine/hifi-boot.js');
|
||||
jasmine.getEnv().addReporter({ jasmineDone: Script.stop });
|
||||
run = function() {
|
||||
print('executing jasmine', Script.resolvePath(''));
|
||||
jasmine.getEnv().execute();
|
||||
};
|
||||
}
|
||||
}
|
||||
run();
|
Loading…
Reference in a new issue