History

merge from upstream

Seth Alves committed 9 years ago

Commit e8a9ff82 parents 9881dd2 546f324463 files · +21986 −6894Browse files

Showing the changes to libraries/script-engine/src/ScriptEngine.cpp (1 of 463 files). Show all

Unified · Split

libraries/script-engine/src/ScriptEngine.cpp+508−206
@@ -34,6 +34,7 @@ · 33 unmodified lines
34#include <AudioConstants.h>34#include <AudioConstants.h>
35#include <AudioEffectOptions.h>35#include <AudioEffectOptions.h>
36#include <AvatarData.h>36#include <AvatarData.h>
37#include <DebugDraw.h>
37#include <EntityScriptingInterface.h>38#include <EntityScriptingInterface.h>
38#include <MessagesClient.h>39#include <MessagesClient.h>
39#include <NetworkAccessManager.h>40#include <NetworkAccessManager.h>
@@ -49,6 +50,7 @@ · 9 unmodified lines
49 50
50#include "ArrayBufferViewClass.h"51#include "ArrayBufferViewClass.h"
51#include "BatchLoader.h"52#include "BatchLoader.h"
53#include "BaseScriptEngine.h"
52#include "DataViewClass.h"54#include "DataViewClass.h"
53#include "EventTypes.h"55#include "EventTypes.h"
54#include "FileScriptingInterface.h" // unzip project56#include "FileScriptingInterface.h" // unzip project
@@ -66,11 +68,18 @@ · 11 unmodified lines
66#include "ModelScriptingInterface.h"68#include "ModelScriptingInterface.h"
67 69
68 70
71#include <Profile.h>
72
69#include "MIDIEvent.h"73#include "MIDIEvent.h"
70 74
71static const QString SCRIPT_EXCEPTION_FORMAT = "[UncaughtException] %1 in %2:%3";
72static const QScriptEngine::QObjectWrapOptions DEFAULT_QOBJECT_WRAP_OPTIONS =75static const QScriptEngine::QObjectWrapOptions DEFAULT_QOBJECT_WRAP_OPTIONS =
73 QScriptEngine::ExcludeDeleteLater | QScriptEngine::ExcludeChildObjects;76 QScriptEngine::ExcludeDeleteLater | QScriptEngine::ExcludeChildObjects;
77static const QScriptValue::PropertyFlags READONLY_PROP_FLAGS { QScriptValue::ReadOnly | QScriptValue::Undeletable };
78static const QScriptValue::PropertyFlags READONLY_HIDDEN_PROP_FLAGS { READONLY_PROP_FLAGS | QScriptValue::SkipInEnumeration };
79
80
81
82static const bool HIFI_AUTOREFRESH_FILE_SCRIPTS { true };
74 83
75Q_DECLARE_METATYPE(QScriptEngine::FunctionSignature)84Q_DECLARE_METATYPE(QScriptEngine::FunctionSignature)
76int functionSignatureMetaID = qRegisterMetaType<QScriptEngine::FunctionSignature>();85int functionSignatureMetaID = qRegisterMetaType<QScriptEngine::FunctionSignature>();
@@ -87,13 +96,9 @@ static QScriptValue debugPrint(QScriptContext* context, QScriptEngine* engine){ · 10 unmodified lines
87 }96 }
88 qCDebug(scriptengineScript).noquote() << "script:print()<<" << message; // noquote() so that \n is treated as newline97 qCDebug(scriptengineScript).noquote() << "script:print()<<" << message; // noquote() so that \n is treated as newline
89 98
90 message = message.replace("\\", "\\\\")99 // FIXME - this approach neeeds revisiting. print() comes here, which ends up calling Script.print?
91 .replace("\n", "\\n")100 engine->globalObject().property("Script").property("print")
92 .replace("\r", "\\r")101 .call(engine->nullValue(), QScriptValueList({ message }));
93 .replace("'", "\\'");
94
95 // FIXME - this approach neeeds revisiting. print() comes here, which ends up doing an evaluate?
96 engine->evaluate("Script.print('" + message + "')");
97 102
98 return QScriptValue();103 return QScriptValue();
99}104}
@@ -137,41 +142,15 @@ QString encodeEntityIdIntoEntityUrl(const QString& url, const QString& entityID) · 37 unmodified lines
137 return url + " [EntityID:" + entityID + "]";142 return url + " [EntityID:" + entityID + "]";
138}143}
139 144
140static bool hasCorrectSyntax(const QScriptProgram& program, ScriptEngine* reportingEngine) {145QString ScriptEngine::logException(const QScriptValue& exception) {
141 const auto syntaxCheck = QScriptEngine::checkSyntax(program.sourceCode());146 auto message = formatException(exception);
142 if (syntaxCheck.state() != QScriptSyntaxCheckResult::Valid) {147 scriptErrorMessage(qPrintable(message));
143 const auto error = syntaxCheck.errorMessage();148 return message;
144 const auto line = QString::number(syntaxCheck.errorLineNumber());
145 const auto column = QString::number(syntaxCheck.errorColumnNumber());
146 const auto message = QString("[SyntaxError] %1 in %2:%3(%4)").arg(error, program.fileName(), line, column);
147 reportingEngine->scriptErrorMessage(qPrintable(message));
148 return false;
149 }
150 return true;
151}
152
153static bool hadUncaughtExceptions(QScriptEngine& engine, const QString& fileName, ScriptEngine* reportingEngine, QString* exceptionMessage = nullptr) {
154 if (engine.hasUncaughtException()) {
155 const auto backtrace = engine.uncaughtExceptionBacktrace();
156 const auto exception = engine.uncaughtException().toString();
157 const auto line = QString::number(engine.uncaughtExceptionLineNumber());
158 engine.clearExceptions();
159
160 QString message = QString(SCRIPT_EXCEPTION_FORMAT).arg(exception, fileName, line);
161 if (!backtrace.empty()) {
162 static const auto lineSeparator = "\n ";
163 message += QString("\n[Backtrace]%1%2").arg(lineSeparator, backtrace.join(lineSeparator));
164 }
165 reportingEngine->scriptErrorMessage(qPrintable(message));
166 if (exceptionMessage) {
167 *exceptionMessage = message;
168 }
169 return true;
170 }
171 return false;
172}149}
173 150
151int ScriptEngine::processLevelMaxRetries { ScriptRequest::MAX_RETRIES };
174ScriptEngine::ScriptEngine(Context context, const QString& scriptContents, const QString& fileNameString) :152ScriptEngine::ScriptEngine(Context context, const QString& scriptContents, const QString& fileNameString) :
153 BaseScriptEngine(),
175 _context(context),154 _context(context),
176 _scriptContents(scriptContents),155 _scriptContents(scriptContents),
177 _timerFunctionMap(),156 _timerFunctionMap(),
@@ -181,10 +160,30 @@ ScriptEngine::ScriptEngine(Context context, const QString& scriptContents, const · 3 unmodified lines
181 DependencyManager::get<ScriptEngines>()->addScriptEngine(this);160 DependencyManager::get<ScriptEngines>()->addScriptEngine(this);
182 161
183 connect(this, &QScriptEngine::signalHandlerException, this, [this](const QScriptValue& exception) {162 connect(this, &QScriptEngine::signalHandlerException, this, [this](const QScriptValue& exception) {
184 hadUncaughtExceptions(*this, _fileNameString, this);163 if (hasUncaughtException()) {
185 });164 // the engine's uncaughtException() seems to produce much better stack traces here
165 emit unhandledException(cloneUncaughtException("signalHandlerException"));
166 clearExceptions();
167 } else {
168 // ... but may not always be available -- so if needed we fallback to the passed exception
169 emit unhandledException(exception);
170 }
171 }, Qt::DirectConnection);
186 172
187 setProcessEventsInterval(MSECS_PER_SECOND);173 setProcessEventsInterval(MSECS_PER_SECOND);
174 if (isEntityServerScript()) {
175 qCDebug(scriptengine) << "isEntityServerScript() -- limiting maxRetries to 1";
176 processLevelMaxRetries = 1;
177 }
178
179 // this is where all unhandled exceptions end up getting logged
180 connect(this, &BaseScriptEngine::unhandledException, this, [this](const QScriptValue& err) {
181 auto output = err.engine() == this ? err : makeError(err);
182 if (!output.property("detail").isValid()) {
183 output.setProperty("detail", "UnhandledException");
184 }
185 logException(output);
186 });
188}187}
189 188
190QString ScriptEngine::getContext() const {189QString ScriptEngine::getContext() const {
@@ -204,13 +203,22 @@ QString ScriptEngine::getContext() const { · 13 unmodified lines
204}203}
205 204
206ScriptEngine::~ScriptEngine() {205ScriptEngine::~ScriptEngine() {
206 // FIXME: are these scriptInfoMessage/scriptWarningMessage segfaulting anybody else at app shutdown?
207#if !defined(Q_OS_LINUX)
207 scriptInfoMessage("Script Engine shutting down:" + getFilename());208 scriptInfoMessage("Script Engine shutting down:" + getFilename());
209#else
210 qCDebug(scriptengine) << "~ScriptEngine()" << this;
211#endif
208 212
209 auto scriptEngines = DependencyManager::get<ScriptEngines>();213 auto scriptEngines = DependencyManager::get<ScriptEngines>();
210 if (scriptEngines) {214 if (scriptEngines) {
211 scriptEngines->removeScriptEngine(this);215 scriptEngines->removeScriptEngine(this);
212 } else {216 } else {
217#if !defined(Q_OS_LINUX)
213 scriptWarningMessage("Script destroyed after ScriptEngines!");218 scriptWarningMessage("Script destroyed after ScriptEngines!");
219#else
220 qCWarning(scriptengine) << ("Script destroyed after ScriptEngines!");
221#endif
214 }222 }
215}223}
216 224
@@ -300,8 +308,14 @@ void ScriptEngine::runDebuggable() { · 83 unmodified lines
300 }308 }
301 }309 }
302 _lastUpdate = now;310 _lastUpdate = now;
303 // Debug and clear exceptions311
304 hadUncaughtExceptions(*this, _fileNameString, this);312 // only clear exceptions if we are not in the middle of evaluating
313 if (!isEvaluating() && hasUncaughtException()) {
314 qCWarning(scriptengine) << __FUNCTION__ << "---------- UNCAUGHT EXCEPTION --------";
315 qCWarning(scriptengine) << "runDebuggable" << uncaughtException().toString();
316 logException(__FUNCTION__);
317 clearExceptions();
318 }
305 });319 });
306 320
307 timer->start(10);321 timer->start(10);
@@ -334,6 +348,15 @@ void ScriptEngine::runInThread() { · 26 unmodified lines
334 workerThread->start();348 workerThread->start();
335}349}
336 350
351void ScriptEngine::executeOnScriptThread(std::function<void()> function, const Qt::ConnectionType& type ) {
352 if (QThread::currentThread() != thread()) {
353 QMetaObject::invokeMethod(this, "executeOnScriptThread", type, Q_ARG(std::function<void()>, function));
354 return;
355 }
356
357 function();
358}
359
337void ScriptEngine::waitTillDoneRunning() {360void ScriptEngine::waitTillDoneRunning() {
338 auto workerThread = thread();361 auto workerThread = thread();
339 362
@@ -399,8 +422,6 @@ QString ScriptEngine::getFilename() const { · 59 unmodified lines
399 return lastPart;422 return lastPart;
400}423}
401 424
402
403// FIXME - switch this to the new model of ScriptCache callbacks
404void ScriptEngine::loadURL(const QUrl& scriptURL, bool reload) {425void ScriptEngine::loadURL(const QUrl& scriptURL, bool reload) {
405 if (_isRunning) {426 if (_isRunning) {
406 return;427 return;
@@ -410,19 +431,27 @@ void ScriptEngine::loadURL(const QUrl& scriptURL, bool reload) { · 3 unmodified lines
410 _fileNameString = url.toString();431 _fileNameString = url.toString();
411 _isReloading = reload;432 _isReloading = reload;
412 433
413 bool isPending;434 const auto maxRetries = 0; // for consistency with previous scriptCache->getScript() behavior
414 auto scriptCache = DependencyManager::get<ScriptCache>();435 auto scriptCache = DependencyManager::get<ScriptCache>();
415 scriptCache->getScript(url, this, isPending, reload);436 scriptCache->getScriptContents(url.toString(), [this](const QString& url, const QString& scriptContents, bool isURL, bool success, const QString&status) {
416}437 qCDebug(scriptengine) << "loadURL" << url << status << QThread::currentThread();
438 if (!success) {
439 scriptErrorMessage("ERROR Loading file (" + status + "):" + url);
440 emit errorLoadingScript(_fileNameString);
441 return;
442 }
417 443
418// FIXME - switch this to the new model of ScriptCache callbacks444 _scriptContents = scriptContents;
419void ScriptEngine::scriptContentsAvailable(const QUrl& url, const QString& scriptContents) {445
420 _scriptContents = scriptContents;446 {
421 static const QString DEBUG_FLAG("#debug");447 static const QString DEBUG_FLAG("#debug");
422 if (QRegularExpression(DEBUG_FLAG).match(scriptContents).hasMatch()) {448 if (QRegularExpression(DEBUG_FLAG).match(scriptContents).hasMatch()) {
423 _debuggable = true;449 qCWarning(scriptengine) << "NOTE: ScriptEngine for " << QUrl(url).fileName() << " will be launched in debug mode";
424 }450 _debuggable = true;
425 emit scriptLoaded(url.toString());451 }
452 }
453 emit scriptLoaded(url);
454 }, reload, maxRetries);
426}455}
427 456
428void ScriptEngine::scriptErrorMessage(const QString& message) {457void ScriptEngine::scriptErrorMessage(const QString& message) {
@@ -440,12 +469,6 @@ void ScriptEngine::scriptInfoMessage(const QString& message) { · 11 unmodified lines
440 emit infoMessage(message);469 emit infoMessage(message);
441}470}
442 471
443// FIXME - switch this to the new model of ScriptCache callbacks
444void ScriptEngine::errorInLoadingScript(const QUrl& url) {
445 scriptErrorMessage("ERROR Loading file:" + url.toString());
446 emit errorLoadingScript(_fileNameString);
447}
448
449// Even though we never pass AnimVariantMap directly to and from javascript, the queued invokeMethod of472// Even though we never pass AnimVariantMap directly to and from javascript, the queued invokeMethod of
450// callAnimationStateHandler requires that the type be registered.473// callAnimationStateHandler requires that the type be registered.
451// These two are meaningful, if we ever do want to use them...474// These two are meaningful, if we ever do want to use them...
@@ -520,6 +543,16 @@ void ScriptEngine::init() { · 68 unmodified lines
520 543
521 auto entityScriptingInterface = DependencyManager::get<EntityScriptingInterface>();544 auto entityScriptingInterface = DependencyManager::get<EntityScriptingInterface>();
522 entityScriptingInterface->init();545 entityScriptingInterface->init();
546 connect(entityScriptingInterface.data(), &EntityScriptingInterface::deletingEntity, this, [this](const EntityItemID& entityID) {
547 if (_entityScripts.contains(entityID)) {
548 if (isEntityScriptRunning(entityID)) {
549 qCWarning(scriptengine) << "deletingEntity while entity script is still running!" << entityID;
550 }
551 _entityScripts.remove(entityID);
552 emit entityScriptDetailsUpdated();
553 }
554 });
555
523 556
524 // register various meta-types557 // register various meta-types
525 registerMetaTypes(this);558 registerMetaTypes(this);
@@ -535,6 +568,7 @@ void ScriptEngine::init() { · 9 unmodified lines
535 qScriptRegisterMetaType(this, EntityItemIDtoScriptValue, EntityItemIDfromScriptValue);568 qScriptRegisterMetaType(this, EntityItemIDtoScriptValue, EntityItemIDfromScriptValue);
536 qScriptRegisterMetaType(this, RayToEntityIntersectionResultToScriptValue, RayToEntityIntersectionResultFromScriptValue);569 qScriptRegisterMetaType(this, RayToEntityIntersectionResultToScriptValue, RayToEntityIntersectionResultFromScriptValue);
537 qScriptRegisterMetaType(this, RayToAvatarIntersectionResultToScriptValue, RayToAvatarIntersectionResultFromScriptValue);570 qScriptRegisterMetaType(this, RayToAvatarIntersectionResultToScriptValue, RayToAvatarIntersectionResultFromScriptValue);
571 qScriptRegisterMetaType(this, AvatarEntityMapToScriptValue, AvatarEntityMapFromScriptValue);
538 qScriptRegisterSequenceMetaType<QVector<QUuid>>(this);572 qScriptRegisterSequenceMetaType<QVector<QUuid>>(this);
539 qScriptRegisterSequenceMetaType<QVector<EntityItemID>>(this);573 qScriptRegisterSequenceMetaType<QVector<EntityItemID>>(this);
540 574
@@ -548,8 +582,7 @@ void ScriptEngine::init() { · 7 unmodified lines
548 QScriptValue webSocketConstructorValue = newFunction(WebSocketClass::constructor);582 QScriptValue webSocketConstructorValue = newFunction(WebSocketClass::constructor);
549 globalObject().setProperty("WebSocket", webSocketConstructorValue);583 globalObject().setProperty("WebSocket", webSocketConstructorValue);
550 584
551 QScriptValue printConstructorValue = newFunction(debugPrint);585 globalObject().setProperty("print", newFunction(debugPrint));
552 globalObject().setProperty("print", printConstructorValue);
553 586
554 QScriptValue audioEffectOptionsConstructorValue = newFunction(AudioEffectOptions::constructor);587 QScriptValue audioEffectOptionsConstructorValue = newFunction(AudioEffectOptions::constructor);
555 globalObject().setProperty("AudioEffectOptions", audioEffectOptionsConstructorValue);588 globalObject().setProperty("AudioEffectOptions", audioEffectOptionsConstructorValue);
@@ -563,6 +596,7 @@ void ScriptEngine::init() { · 7 unmodified lines
563 qScriptRegisterMetaType(this, wscReadyStateToScriptValue, wscReadyStateFromScriptValue);596 qScriptRegisterMetaType(this, wscReadyStateToScriptValue, wscReadyStateFromScriptValue);
564 597
565 registerGlobalObject("Script", this);598 registerGlobalObject("Script", this);
599
566 registerGlobalObject("Audio", &AudioScriptingInterface::getInstance());600 registerGlobalObject("Audio", &AudioScriptingInterface::getInstance());
567 registerGlobalObject("Entities", entityScriptingInterface.data());601 registerGlobalObject("Entities", entityScriptingInterface.data());
568 registerGlobalObject("Quat", &_quatLibrary);602 registerGlobalObject("Quat", &_quatLibrary);
@@ -589,6 +623,8 @@ void ScriptEngine::init() { · 20 unmodified lines
589 registerGlobalObject("Assets", &_assetScriptingInterface);623 registerGlobalObject("Assets", &_assetScriptingInterface);
590 registerGlobalObject("Resources", DependencyManager::get<ResourceScriptingInterface>().data());624 registerGlobalObject("Resources", DependencyManager::get<ResourceScriptingInterface>().data());
591 625
626 registerGlobalObject("DebugDraw", &DebugDraw::getInstance());
627
592 registerGlobalObject("Model", new ModelScriptingInterface(this));628 registerGlobalObject("Model", new ModelScriptingInterface(this));
593 qScriptRegisterMetaType(this, meshToScriptValue, meshFromScriptValue);629 qScriptRegisterMetaType(this, meshToScriptValue, meshFromScriptValue);
594 qScriptRegisterMetaType(this, meshesToScriptValue, meshesFromScriptValue);630 qScriptRegisterMetaType(this, meshesToScriptValue, meshesFromScriptValue);
@@ -833,7 +869,6 @@ void ScriptEngine::addEventHandler(const EntityItemID& entityID, const QString& · 238 unmodified lines
833 handlersForEvent << handlerData; // Note that the same handler can be added many times. See removeEntityEventHandler().869 handlersForEvent << handlerData; // Note that the same handler can be added many times. See removeEntityEventHandler().
834}870}
835 871
836
837QScriptValue ScriptEngine::evaluate(const QString& sourceCode, const QString& fileName, int lineNumber) {872QScriptValue ScriptEngine::evaluate(const QString& sourceCode, const QString& fileName, int lineNumber) {
838 if (DependencyManager::get<ScriptEngines>()->isStopped()) {873 if (DependencyManager::get<ScriptEngines>()->isStopped()) {
839 return QScriptValue(); // bail early874 return QScriptValue(); // bail early
@@ -854,21 +889,40 @@ QScriptValue ScriptEngine::evaluate(const QString& sourceCode, const QString& fi · 14 unmodified lines
854 }889 }
855 890
856 // Check syntax891 // Check syntax
857 const QScriptProgram program(sourceCode, fileName, lineNumber);892 auto syntaxError = lintScript(sourceCode, fileName);
858 if (!hasCorrectSyntax(program, this)) {893 if (syntaxError.isError()) {
859 return QScriptValue();894 if (isEvaluating()) {
895 currentContext()->throwValue(syntaxError);
896 } else {
897 syntaxError.setProperty("detail", "evaluate");
898 emit unhandledException(syntaxError);
899 }
900 return syntaxError;
901 }
902 QScriptProgram program { sourceCode, fileName, lineNumber };
903 if (program.isNull()) {
904 // can this happen?
905 auto err = makeError("could not create QScriptProgram for " + fileName);
906 emit unhandledException(err);
907 return err;
860 }908 }
861 909
862 ++_evaluatesPending;910 QScriptValue result;
863 const auto result = QScriptEngine::evaluate(program);911 {
864 --_evaluatesPending;912 result = BaseScriptEngine::evaluate(program);
865 913 if (!isEvaluating() && hasUncaughtException()) {
866 const auto hadUncaughtException = hadUncaughtExceptions(*this, program.fileName(), this);914 emit unhandledException(cloneUncaughtException(__FUNCTION__));
867 emit evaluationFinished(result, hadUncaughtException);915 clearExceptions();
916 }
917 }
868 return result;918 return result;
869}919}
870 920
871void ScriptEngine::run() {921void ScriptEngine::run() {
922 auto filenameParts = _fileNameString.split("/");
923 auto name = filenameParts.size() > 0 ? filenameParts[filenameParts.size() - 1] : "unknown";
924 PROFILE_SET_THREAD_NAME("Script: " + name);
925
872 if (DependencyManager::get<ScriptEngines>()->isStopped()) {926 if (DependencyManager::get<ScriptEngines>()->isStopped()) {
873 return; // bail early - avoid setting state in init(), as evaluate() will bail too927 return; // bail early - avoid setting state in init(), as evaluate() will bail too
874 }928 }
@@ -880,8 +934,13 @@ void ScriptEngine::run() { · 5 unmodified lines
880 _isRunning = true;934 _isRunning = true;
881 emit runningStateChanged();935 emit runningStateChanged();
882 936
883 QScriptValue result = evaluate(_scriptContents, _fileNameString);937 {
884 938 evaluate(_scriptContents, _fileNameString);
939 if (!isEvaluating() && hasUncaughtException()) {
940 emit unhandledException(cloneUncaughtException(__FUNCTION__));
941 clearExceptions();
942 }
943 }
885#ifdef _WIN32944#ifdef _WIN32
886 // VS13 does not sleep_until unless it uses the system_clock, see:945 // VS13 does not sleep_until unless it uses the system_clock, see:
887 // https://www.reddit.com/r/cpp_questions/comments/3o71ic/sleep_until_not_working_with_a_time_pointsteady/946 // https://www.reddit.com/r/cpp_questions/comments/3o71ic/sleep_until_not_working_with_a_time_pointsteady/
@@ -929,7 +988,10 @@ void ScriptEngine::run() { · 41 unmodified lines
929 bool processedEvents = false;988 bool processedEvents = false;
930 while (!_isFinished && clock::now() < sleepUntil) {989 while (!_isFinished && clock::now() < sleepUntil) {
931 990
932 QCoreApplication::processEvents(); // before we sleep again, give events a chance to process991 {
992 PROFILE_RANGE(script, "processEvents-sleep");
993 QCoreApplication::processEvents(); // before we sleep again, give events a chance to process
994 }
933 processedEvents = true;995 processedEvents = true;
934 996
935 // If after processing events, we're past due, exit asap997 // If after processing events, we're past due, exit asap
@@ -944,6 +1006,8 @@ void ScriptEngine::run() { · 8 unmodified lines
944 std::this_thread::sleep_until(smallSleepUntil);1006 std::this_thread::sleep_until(smallSleepUntil);
945 }1007 }
946 1008
1009 PROFILE_RANGE(script, "ScriptMainLoop");
1010
947#ifdef SCRIPT_DELAY_DEBUG1011#ifdef SCRIPT_DELAY_DEBUG
948 {1012 {
949 auto actuallySleptUntil = clock::now();1013 auto actuallySleptUntil = clock::now();
@@ -967,6 +1031,7 @@ void ScriptEngine::run() { · 17 unmodified lines
967 1031
968 // Only call this if we didn't processEvents as part of waiting for next frame1032 // Only call this if we didn't processEvents as part of waiting for next frame
969 if (!processedEvents) {1033 if (!processedEvents) {
1034 PROFILE_RANGE(script, "processEvents");
970 QCoreApplication::processEvents();1035 QCoreApplication::processEvents();
971 }1036 }
972 1037
@@ -991,7 +1056,10 @@ void ScriptEngine::run() { · 18 unmodified lines
991 float deltaTime = (float) (now - _lastUpdate) / (float) USECS_PER_SECOND;1056 float deltaTime = (float) (now - _lastUpdate) / (float) USECS_PER_SECOND;
992 if (!_isFinished) {1057 if (!_isFinished) {
993 auto preUpdate = clock::now();1058 auto preUpdate = clock::now();
994 emit update(deltaTime);1059 {
1060 PROFILE_RANGE(script, "ScriptUpdate");
1061 emit update(deltaTime);
1062 }
995 auto postUpdate = clock::now();1063 auto postUpdate = clock::now();
996 auto elapsed = (postUpdate - preUpdate);1064 auto elapsed = (postUpdate - preUpdate);
997 totalUpdates += std::chrono::duration_cast<std::chrono::microseconds>(elapsed);1065 totalUpdates += std::chrono::duration_cast<std::chrono::microseconds>(elapsed);
@@ -999,10 +1067,14 @@ void ScriptEngine::run() { · 1 unmodified line
999 }1067 }
1000 _lastUpdate = now;1068 _lastUpdate = now;
1001 1069
1002 // Debug and clear exceptions1070 // only clear exceptions if we are not in the middle of evaluating
1003 hadUncaughtExceptions(*this, _fileNameString, this);1071 if (!isEvaluating() && hasUncaughtException()) {
1072 qCWarning(scriptengine) << __FUNCTION__ << "---------- UNCAUGHT EXCEPTION --------";
1073 qCWarning(scriptengine) << "runInThread" << uncaughtException().toString();
1074 emit unhandledException(cloneUncaughtException(__FUNCTION__));
1075 clearExceptions();
1076 }
1004 }1077 }
1005
1006 scriptInfoMessage("Script Engine stopping:" + getFilename());1078 scriptInfoMessage("Script Engine stopping:" + getFilename());
1007 1079
1008 stopAllTimers(); // make sure all our timers are stopped if the script is ending1080 stopAllTimers(); // make sure all our timers are stopped if the script is ending
@@ -1035,9 +1107,11 @@ void ScriptEngine::run() { · 26 unmodified lines
1035// we want to only call it in our own run "shutdown" processing.1107// we want to only call it in our own run "shutdown" processing.
1036void ScriptEngine::stopAllTimers() {1108void ScriptEngine::stopAllTimers() {
1037 QMutableHashIterator<QTimer*, CallbackData> i(_timerFunctionMap);1109 QMutableHashIterator<QTimer*, CallbackData> i(_timerFunctionMap);
1110 int j {0};
1038 while (i.hasNext()) {1111 while (i.hasNext()) {
1039 i.next();1112 i.next();
1040 QTimer* timer = i.key();1113 QTimer* timer = i.key();
1114 qCDebug(scriptengine) << getFilename() << "stopAllTimers[" << j++ << "]";
1041 stopTimer(timer);1115 stopTimer(timer);
1042 }1116 }
1043}1117}
@@ -1132,11 +1206,11 @@ void ScriptEngine::timerFired() { · 88 unmodified lines
1132 auto postTimer = p_high_resolution_clock::now();1206 auto postTimer = p_high_resolution_clock::now();
1133 auto elapsed = (postTimer - preTimer);1207 auto elapsed = (postTimer - preTimer);
1134 _totalTimerExecution += std::chrono::duration_cast<std::chrono::microseconds>(elapsed);1208 _totalTimerExecution += std::chrono::duration_cast<std::chrono::microseconds>(elapsed);
1135 1209 } else {
1210 qCWarning(scriptengine) << "timerFired -- invalid function" << timerData.function.toVariant().toString();
1136 }1211 }
1137}1212}
1138 1213
1139
1140QObject* ScriptEngine::setupTimerWithInterval(const QScriptValue& function, int intervalMS, bool isSingleShot) {1214QObject* ScriptEngine::setupTimerWithInterval(const QScriptValue& function, int intervalMS, bool isSingleShot) {
1141 // create the timer, add it to the map, and start it1215 // create the timer, add it to the map, and start it
1142 QTimer* newTimer = new QTimer(this);1216 QTimer* newTimer = new QTimer(this);
@@ -1153,7 +1227,7 @@ QObject* ScriptEngine::setupTimerWithInterval(const QScriptValue& function, int · 10 unmodified lines
1153 // make sure the timer stops when the script does1227 // make sure the timer stops when the script does
1154 connect(this, &ScriptEngine::scriptEnding, newTimer, &QTimer::stop);1228 connect(this, &ScriptEngine::scriptEnding, newTimer, &QTimer::stop);
1155 1229
1156 CallbackData timerData = {function, currentEntityIdentifier, currentSandboxURL };1230 CallbackData timerData = { function, currentEntityIdentifier, currentSandboxURL };
1157 _timerFunctionMap.insert(newTimer, timerData);1231 _timerFunctionMap.insert(newTimer, timerData);
1158 1232
1159 newTimer->start(intervalMS);1233 newTimer->start(intervalMS);
@@ -1183,35 +1257,46 @@ void ScriptEngine::stopTimer(QTimer *timer) { · 23 unmodified lines
1183 timer->stop();1257 timer->stop();
1184 _timerFunctionMap.remove(timer);1258 _timerFunctionMap.remove(timer);
1185 delete timer;1259 delete timer;
1260 } else {
1261 qCDebug(scriptengine) << "stopTimer -- not in _timerFunctionMap" << timer;
1186 }1262 }
1187}1263}
1188 1264
1189QUrl ScriptEngine::resolvePath(const QString& include) const {1265QUrl ScriptEngine::resolvePath(const QString& include) const {
1190 QUrl url(include);1266 QUrl url(include);
1191 // first lets check to see if it's already a full URL1267 // first lets check to see if it's already a full URL -- or a Windows path like "c:/"
1192 if (!url.scheme().isEmpty()) {1268 if (include.startsWith("/") || url.scheme().length() == 1) {
1269 url = QUrl::fromLocalFile(include);
1270 }
1271 if (!url.isRelative()) {
1193 return expandScriptUrl(url);1272 return expandScriptUrl(url);
1194 }1273 }
1195 1274
1196 QScriptContextInfo contextInfo { currentContext()->parentContext() };
1197
1198
1199 // we apparently weren't a fully qualified url, so, let's assume we're relative1275 // we apparently weren't a fully qualified url, so, let's assume we're relative
1200 // to the original URL of our script1276 // to the first absolute URL in the JS scope chain
1201 QUrl parentURL = contextInfo.fileName();1277 QUrl parentURL;
1202 if (parentURL.isEmpty()) {1278 auto context = currentContext();
1203 if (_parentURL.isEmpty()) {1279 do {
1204 parentURL = QUrl(_fileNameString);1280 QScriptContextInfo contextInfo { context };
1205 } else {1281 parentURL = QUrl(contextInfo.fileName());
1206 parentURL = QUrl(_parentURL);1282 context = context->parentContext();
1283 } while (parentURL.isRelative() && context);
1284
1285 if (parentURL.isRelative()) {
1286 // fallback to the "include" parent (if defined, this will already be absolute)
1287 parentURL = QUrl(_parentURL);
1288 }
1289
1290 if (parentURL.isRelative()) {
1291 // fallback to the original script engine URL
1292 parentURL = QUrl(_fileNameString);
1293
1294 // if still relative and path-like, then this is probably a local file...
1295 if (parentURL.isRelative() && url.path().contains("/")) {
1296 parentURL = QUrl::fromLocalFile(_fileNameString);
1207 }1297 }
1208 }1298 }
1209 1299
1210 // if the parent URL's scheme is empty, then this is probably a local file...
1211 if (parentURL.scheme().isEmpty()) {
1212 parentURL = QUrl::fromLocalFile(_fileNameString);
1213 }
1214
1215 // at this point we should have a legitimate fully qualified URL for our parent1300 // at this point we should have a legitimate fully qualified URL for our parent
1216 url = expandScriptUrl(parentURL.resolved(url));1301 url = expandScriptUrl(parentURL.resolved(url));
1217 return url;1302 return url;
@@ -1236,22 +1321,7 @@ void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callbac · 18 unmodified lines
1236 return; // bail early1321 return; // bail early
1237 }1322 }
1238 QList<QUrl> urls;1323 QList<QUrl> urls;
1239 bool knowsSensitivity = false;
1240 Qt::CaseSensitivity sensitivity { Qt::CaseSensitive };
1241 auto getSensitivity = [&]() {
1242 if (!knowsSensitivity) {
1243 QString path = currentSandboxURL.path();
1244 QFileInfo upperFI(path.toUpper());
1245 QFileInfo lowerFI(path.toLower());
1246 sensitivity = (upperFI == lowerFI) ? Qt::CaseInsensitive : Qt::CaseSensitive;
1247 knowsSensitivity = true;
1248 }
1249 return sensitivity;
1250 };
1251 1324
1252 // Guard against meaningless query and fragment parts.
1253 // Do NOT use PreferLocalFile as its behavior is unpredictable (e.g., on defaultScriptsLocation())
1254 const auto strippingFlags = QUrl::RemoveFilename | QUrl::RemoveQuery | QUrl::RemoveFragment;
1255 for (QString includeFile : includeFiles) {1325 for (QString includeFile : includeFiles) {
1256 QString file = ResourceManager::normalizeURL(includeFile);1326 QString file = ResourceManager::normalizeURL(includeFile);
1257 QUrl thisURL;1327 QUrl thisURL;
@@ -1268,10 +1338,8 @@ void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callbac · 10 unmodified lines
1268 thisURL = resolvePath(file);1338 thisURL = resolvePath(file);
1269 }1339 }
1270 1340
1271 if (!isStandardLibrary && !currentSandboxURL.isEmpty() && (thisURL.scheme() == "file") &&1341 bool disallowOutsideFiles = thisURL.isLocalFile() && !isStandardLibrary && !currentSandboxURL.isLocalFile();
1272 (currentSandboxURL.scheme() != "file" ||1342 if (disallowOutsideFiles && !PathUtils::isDescendantOf(thisURL, currentSandboxURL)) {
1273 !thisURL.toString(strippingFlags).startsWith(currentSandboxURL.toString(strippingFlags), getSensitivity()))) {
1274
1275 scriptWarningMessage("Script.include() ignoring file path" + thisURL.toString() 1343 scriptWarningMessage("Script.include() ignoring file path" + thisURL.toString()
1276 + "outside of original entity script" + currentSandboxURL.toString());1344 + "outside of original entity script" + currentSandboxURL.toString());
1277 } else {1345 } else {
@@ -1290,12 +1358,12 @@ void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callbac · 12 unmodified lines
1290 EntityItemID capturedEntityIdentifier = currentEntityIdentifier;1358 EntityItemID capturedEntityIdentifier = currentEntityIdentifier;
1291 QUrl capturedSandboxURL = currentSandboxURL;1359 QUrl capturedSandboxURL = currentSandboxURL;
1292 1360
1293 auto evaluateScripts = [=](const QMap<QUrl, QString>& data) {1361 auto evaluateScripts = [=](const QMap<QUrl, QString>& data, const QMap<QUrl, QString>& status) {
1294 auto parentURL = _parentURL;1362 auto parentURL = _parentURL;
1295 for (QUrl url : urls) {1363 for (QUrl url : urls) {
1296 QString contents = data[url];1364 QString contents = data[url];
1297 if (contents.isNull()) {1365 if (contents.isNull()) {
1298 scriptErrorMessage("Error loading file: " + url.toString());1366 scriptErrorMessage("Error loading file (" + status[url] +"): " + url.toString());
1299 } else {1367 } else {
1300 std::lock_guard<std::recursive_mutex> lock(_lock);1368 std::lock_guard<std::recursive_mutex> lock(_lock);
1301 if (!_includedURLs.contains(url)) {1369 if (!_includedURLs.contains(url)) {
@@ -1308,6 +1376,10 @@ void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callbac · 6 unmodified lines
1308 };1376 };
1309 1377
1310 doWithEnvironment(capturedEntityIdentifier, capturedSandboxURL, operation);1378 doWithEnvironment(capturedEntityIdentifier, capturedSandboxURL, operation);
1379 if (hasUncaughtException()) {
1380 emit unhandledException(cloneUncaughtException(__FUNCTION__));
1381 clearExceptions();
1382 }
1311 } else {1383 } else {
1312 scriptWarningMessage("Script.include() skipping evaluation of previously included url:" + url.toString());1384 scriptWarningMessage("Script.include() skipping evaluation of previously included url:" + url.toString());
1313 }1385 }
@@ -1327,7 +1399,7 @@ void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callbac · 13 unmodified lines
1327 // If we are destroyed before the loader completes, make sure to clean it up1399 // If we are destroyed before the loader completes, make sure to clean it up
1328 connect(this, &QObject::destroyed, loader, &QObject::deleteLater);1400 connect(this, &QObject::destroyed, loader, &QObject::deleteLater);
1329 1401
1330 loader->start();1402 loader->start(processLevelMaxRetries);
1331 1403
1332 if (!callback.isFunction() && !loader->isFinished()) {1404 if (!callback.isFunction() && !loader->isFinished()) {
1333 QEventLoop loop;1405 QEventLoop loop;
@@ -1359,7 +1431,7 @@ void ScriptEngine::load(const QString& loadFile) { · 25 unmodified lines
1359 }1431 }
1360 if (!currentEntityIdentifier.isInvalidID()) {1432 if (!currentEntityIdentifier.isInvalidID()) {
1361 scriptWarningMessage("Script.load() from entity script is ignored... loadFile:" 1433 scriptWarningMessage("Script.load() from entity script is ignored... loadFile:"
1362 + loadFile + "parent script:" + getFilename());1434 + loadFile + "parent script:" + getFilename() + "entity: " + currentEntityIdentifier.toString());
1363 return; // bail early1435 return; // bail early
1364 }1436 }
1365 1437
@@ -1399,6 +1471,28 @@ void ScriptEngine::forwardHandlerCall(const EntityItemID& entityID, const QStrin · 33 unmodified lines
1399 }1471 }
1400}1472}
1401 1473
1474int ScriptEngine::getNumRunningEntityScripts() const {
1475 int sum = 0;
1476 for (auto& st : _entityScripts) {
1477 if (st.status == EntityScriptStatus::RUNNING) {
1478 ++sum;
1479 }
1480 }
1481 return sum;
1482}
1483
1484void ScriptEngine::setEntityScriptDetails(const EntityItemID& entityID, const EntityScriptDetails& details) {
1485 _entityScripts[entityID] = details;
1486 emit entityScriptDetailsUpdated();
1487}
1488
1489void ScriptEngine::updateEntityScriptStatus(const EntityItemID& entityID, const EntityScriptStatus &status, const QString& errorInfo) {
1490 EntityScriptDetails &details = _entityScripts[entityID];
1491 details.status = status;
1492 details.errorInfo = errorInfo;
1493 emit entityScriptDetailsUpdated();
1494}
1495
1402bool ScriptEngine::getEntityScriptDetails(const EntityItemID& entityID, EntityScriptDetails &details) const {1496bool ScriptEngine::getEntityScriptDetails(const EntityItemID& entityID, EntityScriptDetails &details) const {
1403 auto it = _entityScripts.constFind(entityID);1497 auto it = _entityScripts.constFind(entityID);
1404 if (it == _entityScripts.constEnd()) {1498 if (it == _entityScripts.constEnd()) {
@@ -1408,28 +1502,171 @@ bool ScriptEngine::getEntityScriptDetails(const EntityItemID& entityID, EntitySc · 3 unmodified lines
1408 return true;1502 return true;
1409}1503}
1410 1504
1411// since all of these operations can be asynch we will always do the actual work in the response handler1505const static EntityItemID BAD_SCRIPT_UUID_PLACEHOLDER { "{20170224-dead-face-0000-cee000021114}" };
1412// for the download1506
1413void ScriptEngine::loadEntityScript(QWeakPointer<ScriptEngine> theEngine, const EntityItemID& entityID, const QString& entityScript, bool forceRedownload) {1507void ScriptEngine::processDeferredEntityLoads(const QString& entityScript, const EntityItemID& leaderID) {
1414 // NOTE: If the script content is not currently in the cache, the LAMBDA here will be called on the Main Thread1508 QList<DeferredLoadEntity> retryLoads;
1415 // which means we're guaranteed that it's not the correct thread for the ScriptEngine. This means1509 QMutableListIterator<DeferredLoadEntity> i(_deferredEntityLoads);
1416 // when we get into entityScriptContentAvailable() we will likely invokeMethod() to get it over1510 while (i.hasNext()) {
1417 // to the "Entities" ScriptEngine thread.1511 auto retry = i.next();
1418 DependencyManager::get<ScriptCache>()->getScriptContents(entityScript, [theEngine, entityID](const QString& scriptOrURL, const QString& contents, bool isURL, bool success) {1512 if (retry.entityScript == entityScript) {
1419 QSharedPointer<ScriptEngine> strongEngine = theEngine.toStrongRef();1513 retryLoads << retry;
1420 if (strongEngine) {1514 i.remove();
1421#ifdef THREAD_DEBUGGING1515 }
1422 qCDebug(scriptengine) << "ScriptEngine::entityScriptContentAvailable() IN LAMBDA contentAvailable on thread ["1516 }
1423 << QThread::currentThread() << "] expected thread [" << strongEngine->thread() << "]";1517 foreach(DeferredLoadEntity retry, retryLoads) {
1518 // check whether entity was since been deleted
1519 if (!_entityScripts.contains(retry.entityID)) {
1520 qCDebug(scriptengine) << "processDeferredEntityLoads -- entity details gone (entity deleted?)"
1521 << retry.entityID;
1522 continue;
1523 }
1524
1525 // check whether entity has since been unloaded or otherwise errored-out
1526 auto details = _entityScripts[retry.entityID];
1527 if (details.status != EntityScriptStatus::PENDING) {
1528 qCDebug(scriptengine) << "processDeferredEntityLoads -- entity status no longer PENDING; "
1529 << retry.entityID << details.status;
1530 continue;
1531 }
1532
1533 // propagate leader's failure reasons to the pending entity
1534 const auto leaderDetails = _entityScripts[leaderID];
1535 if (leaderDetails.status != EntityScriptStatus::RUNNING) {
1536 qCDebug(scriptengine) << QString("... pending load of %1 cancelled (leader: %2 status: %3)")
1537 .arg(retry.entityID.toString()).arg(leaderID.toString()).arg(leaderDetails.status);
1538
1539 auto extraDetail = QString("\n(propagated from %1)").arg(leaderID.toString());
1540 if (leaderDetails.status == EntityScriptStatus::ERROR_LOADING_SCRIPT ||
1541 leaderDetails.status == EntityScriptStatus::ERROR_RUNNING_SCRIPT) {
1542 // propagate same error so User doesn't have to hunt down stampede's leader
1543 updateEntityScriptStatus(retry.entityID, leaderDetails.status, leaderDetails.errorInfo + extraDetail);
1544 } else {
1545 // the leader Entity somehow ended up in some other state (rapid-fire delete or unload could cause)
1546 updateEntityScriptStatus(retry.entityID, EntityScriptStatus::ERROR_LOADING_SCRIPT,
1547 "A previous Entity failed to load using this script URL; reload to try again." + extraDetail);
1548 }
1549 continue;
1550 }
1551
1552 if (_occupiedScriptURLs.contains(retry.entityScript)) {
1553 qCWarning(scriptengine) << "--- SHOULD NOT HAPPEN -- recursive call into processDeferredEntityLoads" << retry.entityScript;
1554 continue;
1555 }
1556
1557 // if we made it here then the leading entity was successful so proceed with normal load
1558 loadEntityScript(retry.entityID, retry.entityScript, false);
1559 }
1560}
1561
1562void ScriptEngine::loadEntityScript(const EntityItemID& entityID, const QString& entityScript, bool forceRedownload) {
1563 if (QThread::currentThread() != thread()) {
1564 QMetaObject::invokeMethod(this, "loadEntityScript",
1565 Q_ARG(const EntityItemID&, entityID),
1566 Q_ARG(const QString&, entityScript),
1567 Q_ARG(bool, forceRedownload)
1568 );
1569 return;
1570 }
1571 PROFILE_RANGE(script, __FUNCTION__);
1572
1573 if (isStopping() || DependencyManager::get<ScriptEngines>()->isStopped()) {
1574 qCDebug(scriptengine) << "loadEntityScript.start " << entityScript << entityID.toString()
1575 << " but isStopping==" << isStopping()
1576 << " || engines->isStopped==" << DependencyManager::get<ScriptEngines>()->isStopped();
1577 return;
1578 }
1579
1580 if (!_entityScripts.contains(entityID)) {
1581 // make sure EntityScriptDetails has an entry for this UUID right away
1582 // (which allows bailing from the loading/provisioning process early if the Entity gets deleted mid-flight)
1583 updateEntityScriptStatus(entityID, EntityScriptStatus::PENDING, "...pending...");
1584 }
1585
1586 // This "occupied" approach allows multiple Entities to boot from the same script URL while still taking
1587 // full advantage of cacheable require modules. This only affects Entities literally coming in back-to-back
1588 // before the first one has time to finish loading.
1589 if (_occupiedScriptURLs.contains(entityScript)) {
1590 auto currentEntityID = _occupiedScriptURLs[entityScript];
1591 if (currentEntityID == BAD_SCRIPT_UUID_PLACEHOLDER) {
1592 if (forceRedownload) {
1593 // script was previously marked unusable, but we're reloading so reset it
1594 _occupiedScriptURLs.remove(entityScript);
1595 } else {
1596 // since not reloading, assume that the exact same input would produce the exact same output again
1597 // note: this state gets reset with "reload all scripts," leaving/returning to a Domain, clear cache, etc.
1598#ifdef DEBUG_ENTITY_STATES
1599 qCDebug(scriptengine) << QString("loadEntityScript.cancelled entity: %1 script: %2 (previous script failure)")
1600 .arg(entityID.toString()).arg(entityScript);
1601#endif
1602 updateEntityScriptStatus(entityID, EntityScriptStatus::ERROR_LOADING_SCRIPT,
1603 "A previous Entity failed to load using this script URL; reload to try again.");
1604 return;
1605 }
1606 } else {
1607 // another entity is busy loading from this script URL so wait for them to finish
1608#ifdef DEBUG_ENTITY_STATES
1609 qCDebug(scriptengine) << QString("loadEntityScript.deferring[%0] entity: %1 script: %2 (waiting on %3)")
1610 .arg(_deferredEntityLoads.size()).arg(entityID.toString()).arg(entityScript).arg(currentEntityID.toString());
1424#endif1611#endif
1425 strongEngine->entityScriptContentAvailable(entityID, scriptOrURL, contents, isURL, success);1612 _deferredEntityLoads.push_back({ entityID, entityScript });
1613 return;
1426 }1614 }
1615 }
1616
1617 // the scriptURL slot is available; flag as in-use
1618 _occupiedScriptURLs[entityScript] = entityID;
1619
1620#ifdef DEBUG_ENTITY_STATES
1621 auto previousStatus = _entityScripts.contains(entityID) ? _entityScripts[entityID].status : EntityScriptStatus::PENDING;
1622 qCDebug(scriptengine) << "loadEntityScript.LOADING: " << entityScript << entityID.toString()
1623 << "(previous: " << previousStatus << ")";
1624#endif
1625
1626 EntityScriptDetails newDetails;
1627 newDetails.scriptText = entityScript;
1628 newDetails.status = EntityScriptStatus::LOADING;
1629 newDetails.definingSandboxURL = currentSandboxURL;
1630 setEntityScriptDetails(entityID, newDetails);
1631
1632 auto scriptCache = DependencyManager::get<ScriptCache>();
1633 // note: see EntityTreeRenderer.cpp for shared pointer lifecycle management
1634 QWeakPointer<ScriptEngine> weakRef(sharedFromThis());
1635 scriptCache->getScriptContents(entityScript,
1636 [this, weakRef, entityScript, entityID](const QString& url, const QString& contents, bool isURL, bool success, const QString& status) {
1637 QSharedPointer<ScriptEngine> strongRef(weakRef);
1638 if (!strongRef) {
1639 qCWarning(scriptengine) << "loadEntityScript.contentAvailable -- ScriptEngine was deleted during getScriptContents!!";
1640 return;
1641 }
1642 if (isStopping()) {
1643#ifdef DEBUG_ENTITY_STATES
1644 qCDebug(scriptengine) << "loadEntityScript.contentAvailable -- stopping";
1645#endif
1646 return;
1647 }
1648 executeOnScriptThread([=]{
1649#ifdef DEBUG_ENTITY_STATES
1650 qCDebug(scriptengine) << "loadEntityScript.contentAvailable" << status << QUrl(url).fileName() << entityID.toString();
1651#endif
1652 if (!isStopping() && _entityScripts.contains(entityID)) {
1653 entityScriptContentAvailable(entityID, url, contents, isURL, success, status);
1654 } else {
1655#ifdef DEBUG_ENTITY_STATES
1656 qCDebug(scriptengine) << "loadEntityScript.contentAvailable -- aborting";
1657#endif
1658 }
1659 // recheck whether us since may have been set to BAD_SCRIPT_UUID_PLACEHOLDER in entityScriptContentAvailable
1660 if (_occupiedScriptURLs.contains(entityScript) && _occupiedScriptURLs[entityScript] == entityID) {
1661 _occupiedScriptURLs.remove(entityScript);
1662 }
1663 });
1427 }, forceRedownload);1664 }, forceRedownload);
1428}1665}
1429 1666
1430// since all of these operations can be asynch we will always do the actual work in the response handler1667// since all of these operations can be asynch we will always do the actual work in the response handler
1431// for the download1668// for the download
1432void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, const QString& scriptOrURL, const QString& contents, bool isURL, bool success) {1669void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, const QString& scriptOrURL, const QString& contents, bool isURL, bool success , const QString& status) {
1433 if (QThread::currentThread() != thread()) {1670 if (QThread::currentThread() != thread()) {
1434#ifdef THREAD_DEBUGGING1671#ifdef THREAD_DEBUGGING
1435 qCDebug(scriptengine) << "*** WARNING *** ScriptEngine::entityScriptContentAvailable() called on wrong thread ["1672 qCDebug(scriptengine) << "*** WARNING *** ScriptEngine::entityScriptContentAvailable() called on wrong thread ["
@@ -1443,7 +1680,8 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co · 7 unmodified lines
1443 Q_ARG(const QString&, scriptOrURL),1680 Q_ARG(const QString&, scriptOrURL),
1444 Q_ARG(const QString&, contents),1681 Q_ARG(const QString&, contents),
1445 Q_ARG(bool, isURL),1682 Q_ARG(bool, isURL),
1446 Q_ARG(bool, success));1683 Q_ARG(bool, success),
1684 Q_ARG(const QString&, status));
1447 return;1685 return;
1448 }1686 }
1449 1687
@@ -1453,26 +1691,51 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co · 3 unmodified lines
1453 1691
1454 auto scriptCache = DependencyManager::get<ScriptCache>();1692 auto scriptCache = DependencyManager::get<ScriptCache>();
1455 bool isFileUrl = isURL && scriptOrURL.startsWith("file://");1693 bool isFileUrl = isURL && scriptOrURL.startsWith("file://");
1456 auto fileName = isURL ? scriptOrURL : "EmbeddedEntityScript";1694 auto fileName = isURL ? scriptOrURL : "about:EmbeddedEntityScript";
1695
1696 const EntityScriptDetails &oldDetails = _entityScripts[entityID];
1697 const QString entityScript = oldDetails.scriptText;
1457 1698
1458 EntityScriptDetails newDetails;1699 EntityScriptDetails newDetails;
1459 newDetails.scriptText = scriptOrURL;1700 newDetails.scriptText = scriptOrURL;
1460 1701
1702 // If an error happens below, we want to update newDetails with the new status info
1703 // and also abort any pending Entity loads that are waiting on the exact same script URL.
1704 auto setError = [&](const QString &errorInfo, const EntityScriptStatus& status) {
1705 newDetails.errorInfo = errorInfo;
1706 newDetails.status = status;
1707 setEntityScriptDetails(entityID, newDetails);
1708
1709#ifdef DEBUG_ENTITY_STATES
1710 qCDebug(scriptengine) << "entityScriptContentAvailable -- flagging " << entityScript << " as BAD_SCRIPT_UUID_PLACEHOLDER";
1711#endif
1712 // flag the original entityScript as unusuable
1713 _occupiedScriptURLs[entityScript] = BAD_SCRIPT_UUID_PLACEHOLDER;
1714 processDeferredEntityLoads(entityScript, entityID);
1715 };
1716
1717 // NETWORK / FILESYSTEM ERRORS
1461 if (!success) {1718 if (!success) {
1462 newDetails.status = ERROR_LOADING_SCRIPT;1719 setError("Failed to load script (" + status + ")", EntityScriptStatus::ERROR_LOADING_SCRIPT);
1463 newDetails.errorInfo = "Failed to load script";
1464 _entityScripts[entityID] = newDetails;
1465 return;1720 return;
1466 }1721 }
1467 1722
1468 QScriptProgram program(contents, fileName);1723 // SYNTAX ERRORS
1469 if (!hasCorrectSyntax(program, this)) {1724 auto syntaxError = lintScript(contents, fileName);
1470 if (!isFileUrl) {1725 if (syntaxError.isError()) {
1471 scriptCache->addScriptToBadScriptList(scriptOrURL);1726 auto message = syntaxError.property("formatted").toString();
1727 if (message.isEmpty()) {
1728 message = syntaxError.toString();
1472 }1729 }
1473 newDetails.status = ERROR_RUNNING_SCRIPT;1730 setError(QString("Bad syntax (%1)").arg(message), EntityScriptStatus::ERROR_RUNNING_SCRIPT);
1474 newDetails.errorInfo = "Bad syntax";1731 syntaxError.setProperty("detail", entityID.toString());
1475 _entityScripts[entityID] = newDetails;1732 emit unhandledException(syntaxError);
1733 return;
1734 }
1735 QScriptProgram program { contents, fileName };
1736 if (program.isNull()) {
1737 setError("Bad program (isNull)", EntityScriptStatus::ERROR_RUNNING_SCRIPT);
1738 emit unhandledException(makeError("program.isNull"));
1476 return; // done processing script1739 return; // done processing script
1477 }1740 }
1478 1741
@@ -1480,33 +1743,44 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co · 1 unmodified line
1480 setParentURL(scriptOrURL);1743 setParentURL(scriptOrURL);
1481 }1744 }
1482 1745
1746 // SANITY/PERFORMANCE CHECK USING SANDBOX
1483 const int SANDBOX_TIMEOUT = 0.25 * MSECS_PER_SECOND;1747 const int SANDBOX_TIMEOUT = 0.25 * MSECS_PER_SECOND;
1484 QScriptEngine sandbox;1748 BaseScriptEngine sandbox;
1485 sandbox.setProcessEventsInterval(SANDBOX_TIMEOUT);1749 sandbox.setProcessEventsInterval(SANDBOX_TIMEOUT);
1486 QScriptValue testConstructor;1750 QScriptValue testConstructor, exception;
1487 {1751 {
1488 QTimer timeout;1752 QTimer timeout;
1489 timeout.setSingleShot(true);1753 timeout.setSingleShot(true);
1490 timeout.start(SANDBOX_TIMEOUT);1754 timeout.start(SANDBOX_TIMEOUT);
1491 connect(&timeout, &QTimer::timeout, [&sandbox, SANDBOX_TIMEOUT]{1755 connect(&timeout, &QTimer::timeout, [&sandbox, SANDBOX_TIMEOUT, scriptOrURL]{
1492 auto context = sandbox.currentContext();1756 auto context = sandbox.currentContext();
1493 if (context) {1757 if (context) {
1758 qCDebug(scriptengine) << "ScriptEngine::entityScriptContentAvailable timeout(" << scriptOrURL << ")";
1759
1494 // Guard against infinite loops and non-performant code1760 // Guard against infinite loops and non-performant code
1495 context->throwError(QString("Timed out (entity constructors are limited to %1ms)").arg(SANDBOX_TIMEOUT));1761 context->throwError(QString("Timed out (entity constructors are limited to %1ms)").arg(SANDBOX_TIMEOUT));
1496 }1762 }
1497 });1763 });
1764
1498 testConstructor = sandbox.evaluate(program);1765 testConstructor = sandbox.evaluate(program);
1499 }
1500 1766
1501 QString exceptionMessage;1767 if (sandbox.hasUncaughtException()) {
1502 if (hadUncaughtExceptions(sandbox, program.fileName(), this, &exceptionMessage)) {1768 exception = sandbox.cloneUncaughtException(QString("(preflight %1)").arg(entityID.toString()));
1503 newDetails.status = ERROR_RUNNING_SCRIPT;1769 sandbox.clearExceptions();
1504 newDetails.errorInfo = exceptionMessage;1770 } else if (testConstructor.isError()) {
1505 _entityScripts[entityID] = newDetails;1771 exception = testConstructor;
1772 }
1773 }
1506 1774
1775 if (exception.isError()) {
1776 // create a local copy using makeError to decouple from the sandbox engine
1777 exception = makeError(exception);
1778 setError(formatException(exception), EntityScriptStatus::ERROR_RUNNING_SCRIPT);
1779 emit unhandledException(exception);
1507 return;1780 return;
1508 }1781 }
1509 1782
1783 // CONSTRUCTOR VIABILITY
1510 if (!testConstructor.isFunction()) {1784 if (!testConstructor.isFunction()) {
1511 QString testConstructorType = QString(testConstructor.toVariant().typeName());1785 QString testConstructorType = QString(testConstructor.toVariant().typeName());
1512 if (testConstructorType == "") {1786 if (testConstructorType == "") {
@@ -1517,39 +1791,53 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co · 4 unmodified lines
1517 if (testConstructorValue.size() > maxTestConstructorValueSize) {1791 if (testConstructorValue.size() > maxTestConstructorValueSize) {
1518 testConstructorValue = testConstructorValue.mid(0, maxTestConstructorValueSize) + "...";1792 testConstructorValue = testConstructorValue.mid(0, maxTestConstructorValueSize) + "...";
1519 }1793 }
1520 scriptErrorMessage("Error -- ScriptEngine::loadEntityScript() entity:" + entityID.toString()1794 auto message = QString("failed to load entity script -- expected a function, got %1, %2")
1521 + "failed to load entity script -- expected a function, got " + testConstructorType1795 .arg(testConstructorType).arg(testConstructorValue);
1522 + "," + testConstructorValue
1523 + "," + scriptOrURL);
1524 1796
1525 if (!isFileUrl) {1797 auto err = makeError(message);
1526 scriptCache->addScriptToBadScriptList(scriptOrURL);1798 err.setProperty("fileName", scriptOrURL);
1527 }1799 err.setProperty("detail", "(constructor " + entityID.toString() + ")");
1528
1529 newDetails.status = ERROR_RUNNING_SCRIPT;
1530 newDetails.errorInfo = "Could not find constructor";
1531 _entityScripts[entityID] = newDetails;
1532 1800
1801 setError("Could not find constructor (" + testConstructorType + ")", EntityScriptStatus::ERROR_RUNNING_SCRIPT);
1802 emit unhandledException(err);
1533 return; // done processing script1803 return; // done processing script
1534 }1804 }
1535 1805
1806 // (this feeds into refreshFileScript)
1536 int64_t lastModified = 0;1807 int64_t lastModified = 0;
1537 if (isFileUrl) {1808 if (isFileUrl) {
1538 QString file = QUrl(scriptOrURL).toLocalFile();1809 QString file = QUrl(scriptOrURL).toLocalFile();
1539 lastModified = (quint64)QFileInfo(file).lastModified().toMSecsSinceEpoch();1810 lastModified = (quint64)QFileInfo(file).lastModified().toMSecsSinceEpoch();
1540 }1811 }
1812
1813 // THE ACTUAL EVALUATION AND CONSTRUCTION
1541 QScriptValue entityScriptConstructor, entityScriptObject;1814 QScriptValue entityScriptConstructor, entityScriptObject;
1542 QUrl sandboxURL = currentSandboxURL.isEmpty() ? scriptOrURL : currentSandboxURL;1815 QUrl sandboxURL = currentSandboxURL.isEmpty() ? scriptOrURL : currentSandboxURL;
1543 auto initialization = [&]{1816 auto initialization = [&]{
1544 entityScriptConstructor = evaluate(contents, fileName);1817 entityScriptConstructor = evaluate(contents, fileName);
1545 entityScriptObject = entityScriptConstructor.construct();1818 entityScriptObject = entityScriptConstructor.construct();
1819
1820 if (hasUncaughtException()) {
1821 entityScriptObject = cloneUncaughtException("(construct " + entityID.toString() + ")");
1822 clearExceptions();
1823 }
1546 };1824 };
1825
1547 doWithEnvironment(entityID, sandboxURL, initialization);1826 doWithEnvironment(entityID, sandboxURL, initialization);
1548 1827
1828 if (entityScriptObject.isError()) {
1829 auto exception = entityScriptObject;
1830 setError(formatException(exception), EntityScriptStatus::ERROR_RUNNING_SCRIPT);
1831 emit unhandledException(exception);
1832 return;
1833 }
1834
1835 // ... AND WE HAVE LIFTOFF
1836 newDetails.status = EntityScriptStatus::RUNNING;
1549 newDetails.scriptObject = entityScriptObject;1837 newDetails.scriptObject = entityScriptObject;
1550 newDetails.lastModified = lastModified;1838 newDetails.lastModified = lastModified;
1551 newDetails.definingSandboxURL = sandboxURL;1839 newDetails.definingSandboxURL = sandboxURL;
1552 _entityScripts[entityID] = newDetails;1840 setEntityScriptDetails(entityID, newDetails);
1553 1841
1554 if (isURL) {1842 if (isURL) {
1555 setParentURL("");1843 setParentURL("");
@@ -1557,6 +1845,9 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co · 1 unmodified line
1557 1845
1558 // if we got this far, then call the preload method1846 // if we got this far, then call the preload method
1559 callEntityScriptMethod(entityID, "preload");1847 callEntityScriptMethod(entityID, "preload");
1848
1849 _occupiedScriptURLs.remove(entityScript);
1850 processDeferredEntityLoads(entityScript, entityID);
1560}1851}
1561 1852
1562void ScriptEngine::unloadEntityScript(const EntityItemID& entityID) {1853void ScriptEngine::unloadEntityScript(const EntityItemID& entityID) {
@@ -1576,11 +1867,25 @@ void ScriptEngine::unloadEntityScript(const EntityItemID& entityID) { · 13 unmodified lines
1576#endif1867#endif
1577 1868
1578 if (_entityScripts.contains(entityID)) {1869 if (_entityScripts.contains(entityID)) {
1579 if (_entityScripts[entityID].status == RUNNING) {1870 const EntityScriptDetails &oldDetails = _entityScripts[entityID];
1871 if (isEntityScriptRunning(entityID)) {
1580 callEntityScriptMethod(entityID, "unload");1872 callEntityScriptMethod(entityID, "unload");
1873 } else {
1874 qCDebug(scriptengine) << "unload called while !running" << entityID << oldDetails.status;
1875 }
1876 if (oldDetails.status != EntityScriptStatus::UNLOADED) {
1877 EntityScriptDetails newDetails;
1878 newDetails.status = EntityScriptStatus::UNLOADED;
1879 newDetails.lastModified = QDateTime::currentMSecsSinceEpoch();
1880 // keep scriptText populated for the current need to "debouce" duplicate calls to unloadEntityScript
1881 newDetails.scriptText = oldDetails.scriptText;
1882 setEntityScriptDetails(entityID, newDetails);
1581 }1883 }
1582 _entityScripts.remove(entityID);
1583 stopAllTimersForEntityScript(entityID);1884 stopAllTimersForEntityScript(entityID);
1885 {
1886 // FIXME: shouldn't have to do this here, but currently something seems to be firing unloads moments after firing initial load requests
1887 processDeferredEntityLoads(oldDetails.scriptText, entityID);
1888 }
1584 }1889 }
1585}1890}
1586 1891
@@ -1597,25 +1902,23 @@ void ScriptEngine::unloadAllEntityScripts() { · 10 unmodified lines
1597 qCDebug(scriptengine) << "ScriptEngine::unloadAllEntityScripts() called on correct thread [" << thread() << "]";1902 qCDebug(scriptengine) << "ScriptEngine::unloadAllEntityScripts() called on correct thread [" << thread() << "]";
1598#endif1903#endif
1599 foreach(const EntityItemID& entityID, _entityScripts.keys()) {1904 foreach(const EntityItemID& entityID, _entityScripts.keys()) {
1600 if (_entityScripts[entityID].status == RUNNING) {1905 unloadEntityScript(entityID);
1601 callEntityScriptMethod(entityID, "unload");
1602 }
1603 }1906 }
1604 _entityScripts.clear();1907 _entityScripts.clear();
1908 emit entityScriptDetailsUpdated();
1909 _occupiedScriptURLs.clear();
1605 1910
1606#ifdef DEBUG_ENGINE_STATE1911#ifdef DEBUG_ENGINE_STATE
1607 qCDebug(scriptengine) << "---- CURRENT STATE OF ENGINE: --------------------------";1912 _debugDump(
1608 QScriptValueIterator it(globalObject());1913 "---- CURRENT STATE OF ENGINE: --------------------------",
1609 while (it.hasNext()) {1914 globalObject(),
1610 it.next();1915 "--------------------------------------------------------"
1611 qCDebug(scriptengine) << it.name() << ":" << it.value().toString();1916 );
1612 }
1613 qCDebug(scriptengine) << "--------------------------------------------------------";
1614#endif // DEBUG_ENGINE_STATE1917#endif // DEBUG_ENGINE_STATE
1615}1918}
1616 1919
1617void ScriptEngine::refreshFileScript(const EntityItemID& entityID) {1920void ScriptEngine::refreshFileScript(const EntityItemID& entityID) {
1618 if (!_entityScripts.contains(entityID)) {1921 if (!HIFI_AUTOREFRESH_FILE_SCRIPTS || !_entityScripts.contains(entityID)) {
1619 return;1922 return;
1620 }1923 }
1621 1924
@@ -1632,17 +1935,7 @@ void ScriptEngine::refreshFileScript(const EntityItemID& entityID) { · 10 unmodified lines
1632 auto lastModified = QFileInfo(filePath).lastModified().toMSecsSinceEpoch();1935 auto lastModified = QFileInfo(filePath).lastModified().toMSecsSinceEpoch();
1633 if (lastModified > details.lastModified) {1936 if (lastModified > details.lastModified) {
1634 scriptInfoMessage("Reloading modified script " + details.scriptText);1937 scriptInfoMessage("Reloading modified script " + details.scriptText);
1635 1938 loadEntityScript(entityID, details.scriptText, true);
1636 QFile file(filePath);
1637 file.open(QIODevice::ReadOnly);
1638 QString scriptContents = QTextStream(&file).readAll();
1639 this->unloadEntityScript(entityID);
1640 this->entityScriptContentAvailable(entityID, details.scriptText, scriptContents, true, true);
1641 if (!_entityScripts.contains(entityID) || _entityScripts[entityID].status != RUNNING) {
1642 scriptWarningMessage("Reload script " + details.scriptText + " failed");
1643 } else {
1644 details = _entityScripts[entityID];
1645 }
1646 }1939 }
1647 }1940 }
1648 recurseGuard = false;1941 recurseGuard = false;
@@ -1666,11 +1959,14 @@ void ScriptEngine::doWithEnvironment(const EntityItemID& entityID, const QUrl& s · 17 unmodified lines
1666#else1959#else
1667 operation();1960 operation();
1668#endif1961#endif
1669 hadUncaughtExceptions(*this, _fileNameString, this);1962 if (!isEvaluating() && hasUncaughtException()) {
1670 1963 emit unhandledException(cloneUncaughtException(!entityID.isNull() ? entityID.toString() : __FUNCTION__));
1964 clearExceptions();
1965 }
1671 currentEntityIdentifier = oldIdentifier;1966 currentEntityIdentifier = oldIdentifier;
1672 currentSandboxURL = oldSandboxURL;1967 currentSandboxURL = oldSandboxURL;
1673}1968}
1969
1674void ScriptEngine::callWithEnvironment(const EntityItemID& entityID, const QUrl& sandboxURL, QScriptValue function, QScriptValue thisObject, QScriptValueList args) {1970void ScriptEngine::callWithEnvironment(const EntityItemID& entityID, const QUrl& sandboxURL, QScriptValue function, QScriptValue thisObject, QScriptValueList args) {
1675 auto operation = [&]() {1971 auto operation = [&]() {
1676 function.call(thisObject, args);1972 function.call(thisObject, args);
@@ -1696,8 +1992,10 @@ void ScriptEngine::callEntityScriptMethod(const EntityItemID& entityID, const QS · 19 unmodified lines
1696 "entityID:" << entityID << "methodName:" << methodName;1992 "entityID:" << entityID << "methodName:" << methodName;
1697#endif1993#endif
1698 1994
1699 refreshFileScript(entityID);1995 if (HIFI_AUTOREFRESH_FILE_SCRIPTS && methodName != "unload") {
1700 if (_entityScripts.contains(entityID) && _entityScripts[entityID].status == RUNNING) {1996 refreshFileScript(entityID);
1997 }
1998 if (isEntityScriptRunning(entityID)) {
1701 EntityScriptDetails details = _entityScripts[entityID];1999 EntityScriptDetails details = _entityScripts[entityID];
1702 QScriptValue entityScript = details.scriptObject; // previously loaded2000 QScriptValue entityScript = details.scriptObject; // previously loaded
1703 if (entityScript.property(methodName).isFunction()) {2001 if (entityScript.property(methodName).isFunction()) {
@@ -1728,8 +2026,10 @@ void ScriptEngine::callEntityScriptMethod(const EntityItemID& entityID, const QS · 24 unmodified lines
1728 "entityID:" << entityID << "methodName:" << methodName << "event: pointerEvent";2026 "entityID:" << entityID << "methodName:" << methodName << "event: pointerEvent";
1729#endif2027#endif
1730 2028
1731 refreshFileScript(entityID);2029 if (HIFI_AUTOREFRESH_FILE_SCRIPTS) {
1732 if (_entityScripts.contains(entityID) && _entityScripts[entityID].status == RUNNING) {2030 refreshFileScript(entityID);
2031 }
2032 if (isEntityScriptRunning(entityID)) {
1733 EntityScriptDetails details = _entityScripts[entityID];2033 EntityScriptDetails details = _entityScripts[entityID];
1734 QScriptValue entityScript = details.scriptObject; // previously loaded2034 QScriptValue entityScript = details.scriptObject; // previously loaded
1735 if (entityScript.property(methodName).isFunction()) {2035 if (entityScript.property(methodName).isFunction()) {
@@ -1741,7 +2041,6 @@ void ScriptEngine::callEntityScriptMethod(const EntityItemID& entityID, const QS · 5 unmodified lines
1741 }2041 }
1742}2042}
1743 2043
1744
1745void ScriptEngine::callEntityScriptMethod(const EntityItemID& entityID, const QString& methodName, const EntityItemID& otherID, const Collision& collision) {2044void ScriptEngine::callEntityScriptMethod(const EntityItemID& entityID, const QString& methodName, const EntityItemID& otherID, const Collision& collision) {
1746 if (QThread::currentThread() != thread()) {2045 if (QThread::currentThread() != thread()) {
1747#ifdef THREAD_DEBUGGING2046#ifdef THREAD_DEBUGGING
@@ -1761,8 +2060,10 @@ void ScriptEngine::callEntityScriptMethod(const EntityItemID& entityID, const QS · 13 unmodified lines
1761 "entityID:" << entityID << "methodName:" << methodName << "otherID:" << otherID << "collision: collision";2060 "entityID:" << entityID << "methodName:" << methodName << "otherID:" << otherID << "collision: collision";
1762#endif2061#endif
1763 2062
1764 refreshFileScript(entityID);2063 if (HIFI_AUTOREFRESH_FILE_SCRIPTS) {
1765 if (_entityScripts.contains(entityID) && _entityScripts[entityID].status == RUNNING) {2064 refreshFileScript(entityID);
2065 }
2066 if (isEntityScriptRunning(entityID)) {
1766 EntityScriptDetails details = _entityScripts[entityID];2067 EntityScriptDetails details = _entityScripts[entityID];
1767 QScriptValue entityScript = details.scriptObject; // previously loaded2068 QScriptValue entityScript = details.scriptObject; // previously loaded
1768 if (entityScript.property(methodName).isFunction()) {2069 if (entityScript.property(methodName).isFunction()) {
@@ -1774,3 +2075,4 @@ void ScriptEngine::callEntityScriptMethod(const EntityItemID& entityID, const QS · 5 unmodified lines
1774 }2075 }
1775 }2076 }
1776}2077}
2078