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
3434 #include <AudioConstants.h>
3535 #include <AudioEffectOptions.h>
3636 #include <AvatarData.h>
37+#include <DebugDraw.h>
3738 #include <EntityScriptingInterface.h>
3839 #include <MessagesClient.h>
3940 #include <NetworkAccessManager.h>
@@ -49,6 +50,7 @@ · 9 unmodified lines
4950
5051 #include "ArrayBufferViewClass.h"
5152 #include "BatchLoader.h"
53+#include "BaseScriptEngine.h"
5254 #include "DataViewClass.h"
5355 #include "EventTypes.h"
5456 #include "FileScriptingInterface.h" // unzip project
@@ -66,11 +68,18 @@ · 11 unmodified lines
6668 #include "ModelScriptingInterface.h"
6769
6870
71+#include <Profile.h>
72+
6973 #include "MIDIEvent.h"
7074
71−static const QString SCRIPT_EXCEPTION_FORMAT = "[UncaughtException] %1 in %2:%3";
7275 static const QScriptEngine::QObjectWrapOptions DEFAULT_QOBJECT_WRAP_OPTIONS =
7376 QScriptEngine::ExcludeDeleteLater | QScriptEngine::ExcludeChildObjects;
77+static const QScriptValue::PropertyFlags READONLY_PROP_FLAGS { QScriptValue::ReadOnly | QScriptValue::Undeletable };
78+static const QScriptValue::PropertyFlags READONLY_HIDDEN_PROP_FLAGS { READONLY_PROP_FLAGS | QScriptValue::SkipInEnumeration };
79+
80+
81+
82+static const bool HIFI_AUTOREFRESH_FILE_SCRIPTS { true };
7483
7584 Q_DECLARE_METATYPE(QScriptEngine::FunctionSignature)
7685 int functionSignatureMetaID = qRegisterMetaType<QScriptEngine::FunctionSignature>();
@@ -87,13 +96,9 @@ static QScriptValue debugPrint(QScriptContext* context, QScriptEngine* engine){ · 10 unmodified lines
8796 }
8897 qCDebug(scriptengineScript).noquote() << "script:print()<<" << message; // noquote() so that \n is treated as newline
8998
90− message = message.replace("\\", "\\\\")
91− .replace("\n", "\\n")
92− .replace("\r", "\\r")
93− .replace("'", "\\'");
94−
95− // FIXME - this approach neeeds revisiting. print() comes here, which ends up doing an evaluate?
96− engine->evaluate("Script.print('" + message + "')");
99+ // FIXME - this approach neeeds revisiting. print() comes here, which ends up calling Script.print?
100+ engine->globalObject().property("Script").property("print")
101+ .call(engine->nullValue(), QScriptValueList({ message }));
97102
98103 return QScriptValue();
99104 }
@@ -137,41 +142,15 @@ QString encodeEntityIdIntoEntityUrl(const QString& url, const QString& entityID) · 37 unmodified lines
137142 return url + " [EntityID:" + entityID + "]";
138143 }
139144
140−static bool hasCorrectSyntax(const QScriptProgram& program, ScriptEngine* reportingEngine) {
141− const auto syntaxCheck = QScriptEngine::checkSyntax(program.sourceCode());
142− if (syntaxCheck.state() != QScriptSyntaxCheckResult::Valid) {
143− const auto error = syntaxCheck.errorMessage();
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−
153−static 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;
145+QString ScriptEngine::logException(const QScriptValue& exception) {
146+ auto message = formatException(exception);
147+ scriptErrorMessage(qPrintable(message));
148+ return message;
172149 }
173150
151+int ScriptEngine::processLevelMaxRetries { ScriptRequest::MAX_RETRIES };
174152 ScriptEngine::ScriptEngine(Context context, const QString& scriptContents, const QString& fileNameString) :
153+ BaseScriptEngine(),
175154 _context(context),
176155 _scriptContents(scriptContents),
177156 _timerFunctionMap(),
@@ -181,10 +160,30 @@ ScriptEngine::ScriptEngine(Context context, const QString& scriptContents, const · 3 unmodified lines
181160 DependencyManager::get<ScriptEngines>()->addScriptEngine(this);
182161
183162 connect(this, &QScriptEngine::signalHandlerException, this, [this](const QScriptValue& exception) {
184− hadUncaughtExceptions(*this, _fileNameString, this);
185− });
163+ if (hasUncaughtException()) {
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);
186172
187173 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+ });
188187 }
189188
190189 QString ScriptEngine::getContext() const {
@@ -204,13 +203,22 @@ QString ScriptEngine::getContext() const { · 13 unmodified lines
204203 }
205204
206205 ScriptEngine::~ScriptEngine() {
206+ // FIXME: are these scriptInfoMessage/scriptWarningMessage segfaulting anybody else at app shutdown?
207+#if !defined(Q_OS_LINUX)
207208 scriptInfoMessage("Script Engine shutting down:" + getFilename());
209+#else
210+ qCDebug(scriptengine) << "~ScriptEngine()" << this;
211+#endif
208212
209213 auto scriptEngines = DependencyManager::get<ScriptEngines>();
210214 if (scriptEngines) {
211215 scriptEngines->removeScriptEngine(this);
212216 } else {
217+#if !defined(Q_OS_LINUX)
213218 scriptWarningMessage("Script destroyed after ScriptEngines!");
219+#else
220+ qCWarning(scriptengine) << ("Script destroyed after ScriptEngines!");
221+#endif
214222 }
215223 }
216224
@@ -300,8 +308,14 @@ void ScriptEngine::runDebuggable() { · 83 unmodified lines
300308 }
301309 }
302310 _lastUpdate = now;
303− // Debug and clear exceptions
304− hadUncaughtExceptions(*this, _fileNameString, this);
311+
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+ }
305319 });
306320
307321 timer->start(10);
@@ -334,6 +348,15 @@ void ScriptEngine::runInThread() { · 26 unmodified lines
334348 workerThread->start();
335349 }
336350
351+void 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+
337360 void ScriptEngine::waitTillDoneRunning() {
338361 auto workerThread = thread();
339362
@@ -399,8 +422,6 @@ QString ScriptEngine::getFilename() const { · 59 unmodified lines
399422 return lastPart;
400423 }
401424
402−
403−// FIXME - switch this to the new model of ScriptCache callbacks
404425 void ScriptEngine::loadURL(const QUrl& scriptURL, bool reload) {
405426 if (_isRunning) {
406427 return;
@@ -410,19 +431,27 @@ void ScriptEngine::loadURL(const QUrl& scriptURL, bool reload) { · 3 unmodified lines
410431 _fileNameString = url.toString();
411432 _isReloading = reload;
412433
413− bool isPending;
434+ const auto maxRetries = 0; // for consistency with previous scriptCache->getScript() behavior
414435 auto scriptCache = DependencyManager::get<ScriptCache>();
415− scriptCache->getScript(url, this, isPending, reload);
416−}
436+ scriptCache->getScriptContents(url.toString(), [this](const QString& url, const QString& scriptContents, bool isURL, bool success, const QString&status) {
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+ }
417443
418−// FIXME - switch this to the new model of ScriptCache callbacks
419−void ScriptEngine::scriptContentsAvailable(const QUrl& url, const QString& scriptContents) {
420− _scriptContents = scriptContents;
421− static const QString DEBUG_FLAG("#debug");
422− if (QRegularExpression(DEBUG_FLAG).match(scriptContents).hasMatch()) {
423− _debuggable = true;
424− }
425− emit scriptLoaded(url.toString());
444+ _scriptContents = scriptContents;
445+
446+ {
447+ static const QString DEBUG_FLAG("#debug");
448+ if (QRegularExpression(DEBUG_FLAG).match(scriptContents).hasMatch()) {
449+ qCWarning(scriptengine) << "NOTE: ScriptEngine for " << QUrl(url).fileName() << " will be launched in debug mode";
450+ _debuggable = true;
451+ }
452+ }
453+ emit scriptLoaded(url);
454+ }, reload, maxRetries);
426455 }
427456
428457 void ScriptEngine::scriptErrorMessage(const QString& message) {
@@ -440,12 +469,6 @@ void ScriptEngine::scriptInfoMessage(const QString& message) { · 11 unmodified lines
440469 emit infoMessage(message);
441470 }
442471
443−// FIXME - switch this to the new model of ScriptCache callbacks
444−void ScriptEngine::errorInLoadingScript(const QUrl& url) {
445− scriptErrorMessage("ERROR Loading file:" + url.toString());
446− emit errorLoadingScript(_fileNameString);
447−}
448−
449472 // Even though we never pass AnimVariantMap directly to and from javascript, the queued invokeMethod of
450473 // callAnimationStateHandler requires that the type be registered.
451474 // These two are meaningful, if we ever do want to use them...
@@ -520,6 +543,16 @@ void ScriptEngine::init() { · 68 unmodified lines
520543
521544 auto entityScriptingInterface = DependencyManager::get<EntityScriptingInterface>();
522545 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+
523556
524557 // register various meta-types
525558 registerMetaTypes(this);
@@ -535,6 +568,7 @@ void ScriptEngine::init() { · 9 unmodified lines
535568 qScriptRegisterMetaType(this, EntityItemIDtoScriptValue, EntityItemIDfromScriptValue);
536569 qScriptRegisterMetaType(this, RayToEntityIntersectionResultToScriptValue, RayToEntityIntersectionResultFromScriptValue);
537570 qScriptRegisterMetaType(this, RayToAvatarIntersectionResultToScriptValue, RayToAvatarIntersectionResultFromScriptValue);
571+ qScriptRegisterMetaType(this, AvatarEntityMapToScriptValue, AvatarEntityMapFromScriptValue);
538572 qScriptRegisterSequenceMetaType<QVector<QUuid>>(this);
539573 qScriptRegisterSequenceMetaType<QVector<EntityItemID>>(this);
540574
@@ -548,8 +582,7 @@ void ScriptEngine::init() { · 7 unmodified lines
548582 QScriptValue webSocketConstructorValue = newFunction(WebSocketClass::constructor);
549583 globalObject().setProperty("WebSocket", webSocketConstructorValue);
550584
551− QScriptValue printConstructorValue = newFunction(debugPrint);
552− globalObject().setProperty("print", printConstructorValue);
585+ globalObject().setProperty("print", newFunction(debugPrint));
553586
554587 QScriptValue audioEffectOptionsConstructorValue = newFunction(AudioEffectOptions::constructor);
555588 globalObject().setProperty("AudioEffectOptions", audioEffectOptionsConstructorValue);
@@ -563,6 +596,7 @@ void ScriptEngine::init() { · 7 unmodified lines
563596 qScriptRegisterMetaType(this, wscReadyStateToScriptValue, wscReadyStateFromScriptValue);
564597
565598 registerGlobalObject("Script", this);
599+
566600 registerGlobalObject("Audio", &AudioScriptingInterface::getInstance());
567601 registerGlobalObject("Entities", entityScriptingInterface.data());
568602 registerGlobalObject("Quat", &_quatLibrary);
@@ -589,6 +623,8 @@ void ScriptEngine::init() { · 20 unmodified lines
589623 registerGlobalObject("Assets", &_assetScriptingInterface);
590624 registerGlobalObject("Resources", DependencyManager::get<ResourceScriptingInterface>().data());
591625
626+ registerGlobalObject("DebugDraw", &DebugDraw::getInstance());
627+
592628 registerGlobalObject("Model", new ModelScriptingInterface(this));
593629 qScriptRegisterMetaType(this, meshToScriptValue, meshFromScriptValue);
594630 qScriptRegisterMetaType(this, meshesToScriptValue, meshesFromScriptValue);
@@ -833,7 +869,6 @@ void ScriptEngine::addEventHandler(const EntityItemID& entityID, const QString& · 238 unmodified lines
833869 handlersForEvent << handlerData; // Note that the same handler can be added many times. See removeEntityEventHandler().
834870 }
835871
836−
837872 QScriptValue ScriptEngine::evaluate(const QString& sourceCode, const QString& fileName, int lineNumber) {
838873 if (DependencyManager::get<ScriptEngines>()->isStopped()) {
839874 return QScriptValue(); // bail early
@@ -854,21 +889,40 @@ QScriptValue ScriptEngine::evaluate(const QString& sourceCode, const QString& fi · 14 unmodified lines
854889 }
855890
856891 // Check syntax
857− const QScriptProgram program(sourceCode, fileName, lineNumber);
858− if (!hasCorrectSyntax(program, this)) {
859− return QScriptValue();
892+ auto syntaxError = lintScript(sourceCode, fileName);
893+ if (syntaxError.isError()) {
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;
860908 }
861909
862− ++_evaluatesPending;
863− const auto result = QScriptEngine::evaluate(program);
864− --_evaluatesPending;
865−
866− const auto hadUncaughtException = hadUncaughtExceptions(*this, program.fileName(), this);
867− emit evaluationFinished(result, hadUncaughtException);
910+ QScriptValue result;
911+ {
912+ result = BaseScriptEngine::evaluate(program);
913+ if (!isEvaluating() && hasUncaughtException()) {
914+ emit unhandledException(cloneUncaughtException(__FUNCTION__));
915+ clearExceptions();
916+ }
917+ }
868918 return result;
869919 }
870920
871921 void 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+
872926 if (DependencyManager::get<ScriptEngines>()->isStopped()) {
873927 return; // bail early - avoid setting state in init(), as evaluate() will bail too
874928 }
@@ -880,8 +934,13 @@ void ScriptEngine::run() { · 5 unmodified lines
880934 _isRunning = true;
881935 emit runningStateChanged();
882936
883− QScriptValue result = evaluate(_scriptContents, _fileNameString);
884−
937+ {
938+ evaluate(_scriptContents, _fileNameString);
939+ if (!isEvaluating() && hasUncaughtException()) {
940+ emit unhandledException(cloneUncaughtException(__FUNCTION__));
941+ clearExceptions();
942+ }
943+ }
885944 #ifdef _WIN32
886945 // VS13 does not sleep_until unless it uses the system_clock, see:
887946 // 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
929988 bool processedEvents = false;
930989 while (!_isFinished && clock::now() < sleepUntil) {
931990
932− QCoreApplication::processEvents(); // before we sleep again, give events a chance to process
991+ {
992+ PROFILE_RANGE(script, "processEvents-sleep");
993+ QCoreApplication::processEvents(); // before we sleep again, give events a chance to process
994+ }
933995 processedEvents = true;
934996
935997 // If after processing events, we're past due, exit asap
@@ -944,6 +1006,8 @@ void ScriptEngine::run() { · 8 unmodified lines
9441006 std::this_thread::sleep_until(smallSleepUntil);
9451007 }
9461008
1009+ PROFILE_RANGE(script, "ScriptMainLoop");
1010+
9471011 #ifdef SCRIPT_DELAY_DEBUG
9481012 {
9491013 auto actuallySleptUntil = clock::now();
@@ -967,6 +1031,7 @@ void ScriptEngine::run() { · 17 unmodified lines
9671031
9681032 // Only call this if we didn't processEvents as part of waiting for next frame
9691033 if (!processedEvents) {
1034+ PROFILE_RANGE(script, "processEvents");
9701035 QCoreApplication::processEvents();
9711036 }
9721037
@@ -991,7 +1056,10 @@ void ScriptEngine::run() { · 18 unmodified lines
9911056 float deltaTime = (float) (now - _lastUpdate) / (float) USECS_PER_SECOND;
9921057 if (!_isFinished) {
9931058 auto preUpdate = clock::now();
994− emit update(deltaTime);
1059+ {
1060+ PROFILE_RANGE(script, "ScriptUpdate");
1061+ emit update(deltaTime);
1062+ }
9951063 auto postUpdate = clock::now();
9961064 auto elapsed = (postUpdate - preUpdate);
9971065 totalUpdates += std::chrono::duration_cast<std::chrono::microseconds>(elapsed);
@@ -999,10 +1067,14 @@ void ScriptEngine::run() { · 1 unmodified line
9991067 }
10001068 _lastUpdate = now;
10011069
1002− // Debug and clear exceptions
1003− hadUncaughtExceptions(*this, _fileNameString, this);
1070+ // only clear exceptions if we are not in the middle of evaluating
1071+ if (!isEvaluating() && hasUncaughtException()) {
1072+ qCWarning(scriptengine) << __FUNCTION__ << "---------- UNCAUGHT EXCEPTION --------";
1073+ qCWarning(scriptengine) << "runInThread" << uncaughtException().toString();
1074+ emit unhandledException(cloneUncaughtException(__FUNCTION__));
1075+ clearExceptions();
1076+ }
10041077 }
1005−
10061078 scriptInfoMessage("Script Engine stopping:" + getFilename());
10071079
10081080 stopAllTimers(); // make sure all our timers are stopped if the script is ending
@@ -1035,9 +1107,11 @@ void ScriptEngine::run() { · 26 unmodified lines
10351107 // we want to only call it in our own run "shutdown" processing.
10361108 void ScriptEngine::stopAllTimers() {
10371109 QMutableHashIterator<QTimer*, CallbackData> i(_timerFunctionMap);
1110+ int j {0};
10381111 while (i.hasNext()) {
10391112 i.next();
10401113 QTimer* timer = i.key();
1114+ qCDebug(scriptengine) << getFilename() << "stopAllTimers[" << j++ << "]";
10411115 stopTimer(timer);
10421116 }
10431117 }
@@ -1132,11 +1206,11 @@ void ScriptEngine::timerFired() { · 88 unmodified lines
11321206 auto postTimer = p_high_resolution_clock::now();
11331207 auto elapsed = (postTimer - preTimer);
11341208 _totalTimerExecution += std::chrono::duration_cast<std::chrono::microseconds>(elapsed);
1135−
1209+ } else {
1210+ qCWarning(scriptengine) << "timerFired -- invalid function" << timerData.function.toVariant().toString();
11361211 }
11371212 }
11381213
1139−
11401214 QObject* ScriptEngine::setupTimerWithInterval(const QScriptValue& function, int intervalMS, bool isSingleShot) {
11411215 // create the timer, add it to the map, and start it
11421216 QTimer* newTimer = new QTimer(this);
@@ -1153,7 +1227,7 @@ QObject* ScriptEngine::setupTimerWithInterval(const QScriptValue& function, int · 10 unmodified lines
11531227 // make sure the timer stops when the script does
11541228 connect(this, &ScriptEngine::scriptEnding, newTimer, &QTimer::stop);
11551229
1156− CallbackData timerData = {function, currentEntityIdentifier, currentSandboxURL };
1230+ CallbackData timerData = { function, currentEntityIdentifier, currentSandboxURL };
11571231 _timerFunctionMap.insert(newTimer, timerData);
11581232
11591233 newTimer->start(intervalMS);
@@ -1183,35 +1257,46 @@ void ScriptEngine::stopTimer(QTimer *timer) { · 23 unmodified lines
11831257 timer->stop();
11841258 _timerFunctionMap.remove(timer);
11851259 delete timer;
1260+ } else {
1261+ qCDebug(scriptengine) << "stopTimer -- not in _timerFunctionMap" << timer;
11861262 }
11871263 }
11881264
11891265 QUrl ScriptEngine::resolvePath(const QString& include) const {
11901266 QUrl url(include);
1191− // first lets check to see if it's already a full URL
1192− if (!url.scheme().isEmpty()) {
1267+ // first lets check to see if it's already a full URL -- or a Windows path like "c:/"
1268+ if (include.startsWith("/") || url.scheme().length() == 1) {
1269+ url = QUrl::fromLocalFile(include);
1270+ }
1271+ if (!url.isRelative()) {
11931272 return expandScriptUrl(url);
11941273 }
11951274
1196− QScriptContextInfo contextInfo { currentContext()->parentContext() };
1197−
1198−
11991275 // we apparently weren't a fully qualified url, so, let's assume we're relative
1200− // to the original URL of our script
1201− QUrl parentURL = contextInfo.fileName();
1202− if (parentURL.isEmpty()) {
1203− if (_parentURL.isEmpty()) {
1204− parentURL = QUrl(_fileNameString);
1205− } else {
1206− parentURL = QUrl(_parentURL);
1276+ // to the first absolute URL in the JS scope chain
1277+ QUrl parentURL;
1278+ auto context = currentContext();
1279+ do {
1280+ QScriptContextInfo contextInfo { context };
1281+ parentURL = QUrl(contextInfo.fileName());
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);
12071297 }
12081298 }
12091299
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−
12151300 // at this point we should have a legitimate fully qualified URL for our parent
12161301 url = expandScriptUrl(parentURL.resolved(url));
12171302 return url;
@@ -1236,22 +1321,7 @@ void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callbac · 18 unmodified lines
12361321 return; // bail early
12371322 }
12381323 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− };
12511324
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;
12551325 for (QString includeFile : includeFiles) {
12561326 QString file = ResourceManager::normalizeURL(includeFile);
12571327 QUrl thisURL;
@@ -1268,10 +1338,8 @@ void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callbac · 10 unmodified lines
12681338 thisURL = resolvePath(file);
12691339 }
12701340
1271− if (!isStandardLibrary && !currentSandboxURL.isEmpty() && (thisURL.scheme() == "file") &&
1272− (currentSandboxURL.scheme() != "file" ||
1273− !thisURL.toString(strippingFlags).startsWith(currentSandboxURL.toString(strippingFlags), getSensitivity()))) {
1274−
1341+ bool disallowOutsideFiles = thisURL.isLocalFile() && !isStandardLibrary && !currentSandboxURL.isLocalFile();
1342+ if (disallowOutsideFiles && !PathUtils::isDescendantOf(thisURL, currentSandboxURL)) {
12751343 scriptWarningMessage("Script.include() ignoring file path" + thisURL.toString()
12761344 + "outside of original entity script" + currentSandboxURL.toString());
12771345 } else {
@@ -1290,12 +1358,12 @@ void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callbac · 12 unmodified lines
12901358 EntityItemID capturedEntityIdentifier = currentEntityIdentifier;
12911359 QUrl capturedSandboxURL = currentSandboxURL;
12921360
1293− auto evaluateScripts = [=](const QMap<QUrl, QString>& data) {
1361+ auto evaluateScripts = [=](const QMap<QUrl, QString>& data, const QMap<QUrl, QString>& status) {
12941362 auto parentURL = _parentURL;
12951363 for (QUrl url : urls) {
12961364 QString contents = data[url];
12971365 if (contents.isNull()) {
1298− scriptErrorMessage("Error loading file: " + url.toString());
1366+ scriptErrorMessage("Error loading file (" + status[url] +"): " + url.toString());
12991367 } else {
13001368 std::lock_guard<std::recursive_mutex> lock(_lock);
13011369 if (!_includedURLs.contains(url)) {
@@ -1308,6 +1376,10 @@ void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callbac · 6 unmodified lines
13081376 };
13091377
13101378 doWithEnvironment(capturedEntityIdentifier, capturedSandboxURL, operation);
1379+ if (hasUncaughtException()) {
1380+ emit unhandledException(cloneUncaughtException(__FUNCTION__));
1381+ clearExceptions();
1382+ }
13111383 } else {
13121384 scriptWarningMessage("Script.include() skipping evaluation of previously included url:" + url.toString());
13131385 }
@@ -1327,7 +1399,7 @@ void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callbac · 13 unmodified lines
13271399 // If we are destroyed before the loader completes, make sure to clean it up
13281400 connect(this, &QObject::destroyed, loader, &QObject::deleteLater);
13291401
1330− loader->start();
1402+ loader->start(processLevelMaxRetries);
13311403
13321404 if (!callback.isFunction() && !loader->isFinished()) {
13331405 QEventLoop loop;
@@ -1359,7 +1431,7 @@ void ScriptEngine::load(const QString& loadFile) { · 25 unmodified lines
13591431 }
13601432 if (!currentEntityIdentifier.isInvalidID()) {
13611433 scriptWarningMessage("Script.load() from entity script is ignored... loadFile:"
1362− + loadFile + "parent script:" + getFilename());
1434+ + loadFile + "parent script:" + getFilename() + "entity: " + currentEntityIdentifier.toString());
13631435 return; // bail early
13641436 }
13651437
@@ -1399,6 +1471,28 @@ void ScriptEngine::forwardHandlerCall(const EntityItemID& entityID, const QStrin · 33 unmodified lines
13991471 }
14001472 }
14011473
1474+int 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+
1484+void ScriptEngine::setEntityScriptDetails(const EntityItemID& entityID, const EntityScriptDetails& details) {
1485+ _entityScripts[entityID] = details;
1486+ emit entityScriptDetailsUpdated();
1487+}
1488+
1489+void 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+
14021496 bool ScriptEngine::getEntityScriptDetails(const EntityItemID& entityID, EntityScriptDetails &details) const {
14031497 auto it = _entityScripts.constFind(entityID);
14041498 if (it == _entityScripts.constEnd()) {
@@ -1408,28 +1502,171 @@ bool ScriptEngine::getEntityScriptDetails(const EntityItemID& entityID, EntitySc · 3 unmodified lines
14081502 return true;
14091503 }
14101504
1411−// since all of these operations can be asynch we will always do the actual work in the response handler
1412−// for the download
1413−void ScriptEngine::loadEntityScript(QWeakPointer<ScriptEngine> theEngine, const EntityItemID& entityID, const QString& entityScript, bool forceRedownload) {
1414− // NOTE: If the script content is not currently in the cache, the LAMBDA here will be called on the Main Thread
1415− // which means we're guaranteed that it's not the correct thread for the ScriptEngine. This means
1416− // when we get into entityScriptContentAvailable() we will likely invokeMethod() to get it over
1417− // to the "Entities" ScriptEngine thread.
1418− DependencyManager::get<ScriptCache>()->getScriptContents(entityScript, [theEngine, entityID](const QString& scriptOrURL, const QString& contents, bool isURL, bool success) {
1419− QSharedPointer<ScriptEngine> strongEngine = theEngine.toStrongRef();
1420− if (strongEngine) {
1421−#ifdef THREAD_DEBUGGING
1422− qCDebug(scriptengine) << "ScriptEngine::entityScriptContentAvailable() IN LAMBDA contentAvailable on thread ["
1423− << QThread::currentThread() << "] expected thread [" << strongEngine->thread() << "]";
1505+const static EntityItemID BAD_SCRIPT_UUID_PLACEHOLDER { "{20170224-dead-face-0000-cee000021114}" };
1506+
1507+void ScriptEngine::processDeferredEntityLoads(const QString& entityScript, const EntityItemID& leaderID) {
1508+ QList<DeferredLoadEntity> retryLoads;
1509+ QMutableListIterator<DeferredLoadEntity> i(_deferredEntityLoads);
1510+ while (i.hasNext()) {
1511+ auto retry = i.next();
1512+ if (retry.entityScript == entityScript) {
1513+ retryLoads << retry;
1514+ i.remove();
1515+ }
1516+ }
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+
1562+void 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());
14241611 #endif
1425− strongEngine->entityScriptContentAvailable(entityID, scriptOrURL, contents, isURL, success);
1612+ _deferredEntityLoads.push_back({ entityID, entityScript });
1613+ return;
14261614 }
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+ });
14271664 }, forceRedownload);
14281665 }
14291666
14301667 // since all of these operations can be asynch we will always do the actual work in the response handler
14311668 // for the download
1432−void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, const QString& scriptOrURL, const QString& contents, bool isURL, bool success) {
1669+void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, const QString& scriptOrURL, const QString& contents, bool isURL, bool success , const QString& status) {
14331670 if (QThread::currentThread() != thread()) {
14341671 #ifdef THREAD_DEBUGGING
14351672 qCDebug(scriptengine) << "*** WARNING *** ScriptEngine::entityScriptContentAvailable() called on wrong thread ["
@@ -1443,7 +1680,8 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co · 7 unmodified lines
14431680 Q_ARG(const QString&, scriptOrURL),
14441681 Q_ARG(const QString&, contents),
14451682 Q_ARG(bool, isURL),
1446− Q_ARG(bool, success));
1683+ Q_ARG(bool, success),
1684+ Q_ARG(const QString&, status));
14471685 return;
14481686 }
14491687
@@ -1453,26 +1691,51 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co · 3 unmodified lines
14531691
14541692 auto scriptCache = DependencyManager::get<ScriptCache>();
14551693 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;
14571698
14581699 EntityScriptDetails newDetails;
14591700 newDetails.scriptText = scriptOrURL;
14601701
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
14611718 if (!success) {
1462− newDetails.status = ERROR_LOADING_SCRIPT;
1463− newDetails.errorInfo = "Failed to load script";
1464− _entityScripts[entityID] = newDetails;
1719+ setError("Failed to load script (" + status + ")", EntityScriptStatus::ERROR_LOADING_SCRIPT);
14651720 return;
14661721 }
14671722
1468− QScriptProgram program(contents, fileName);
1469− if (!hasCorrectSyntax(program, this)) {
1470− if (!isFileUrl) {
1471− scriptCache->addScriptToBadScriptList(scriptOrURL);
1723+ // SYNTAX ERRORS
1724+ auto syntaxError = lintScript(contents, fileName);
1725+ if (syntaxError.isError()) {
1726+ auto message = syntaxError.property("formatted").toString();
1727+ if (message.isEmpty()) {
1728+ message = syntaxError.toString();
14721729 }
1473− newDetails.status = ERROR_RUNNING_SCRIPT;
1474− newDetails.errorInfo = "Bad syntax";
1475− _entityScripts[entityID] = newDetails;
1730+ setError(QString("Bad syntax (%1)").arg(message), EntityScriptStatus::ERROR_RUNNING_SCRIPT);
1731+ syntaxError.setProperty("detail", entityID.toString());
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"));
14761739 return; // done processing script
14771740 }
14781741
@@ -1480,33 +1743,44 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co · 1 unmodified line
14801743 setParentURL(scriptOrURL);
14811744 }
14821745
1746+ // SANITY/PERFORMANCE CHECK USING SANDBOX
14831747 const int SANDBOX_TIMEOUT = 0.25 * MSECS_PER_SECOND;
1484− QScriptEngine sandbox;
1748+ BaseScriptEngine sandbox;
14851749 sandbox.setProcessEventsInterval(SANDBOX_TIMEOUT);
1486− QScriptValue testConstructor;
1750+ QScriptValue testConstructor, exception;
14871751 {
14881752 QTimer timeout;
14891753 timeout.setSingleShot(true);
14901754 timeout.start(SANDBOX_TIMEOUT);
1491− connect(&timeout, &QTimer::timeout, [&sandbox, SANDBOX_TIMEOUT]{
1755+ connect(&timeout, &QTimer::timeout, [&sandbox, SANDBOX_TIMEOUT, scriptOrURL]{
14921756 auto context = sandbox.currentContext();
14931757 if (context) {
1758+ qCDebug(scriptengine) << "ScriptEngine::entityScriptContentAvailable timeout(" << scriptOrURL << ")";
1759+
14941760 // Guard against infinite loops and non-performant code
14951761 context->throwError(QString("Timed out (entity constructors are limited to %1ms)").arg(SANDBOX_TIMEOUT));
14961762 }
14971763 });
1764+
14981765 testConstructor = sandbox.evaluate(program);
1499− }
15001766
1501− QString exceptionMessage;
1502− if (hadUncaughtExceptions(sandbox, program.fileName(), this, &exceptionMessage)) {
1503− newDetails.status = ERROR_RUNNING_SCRIPT;
1504− newDetails.errorInfo = exceptionMessage;
1505− _entityScripts[entityID] = newDetails;
1767+ if (sandbox.hasUncaughtException()) {
1768+ exception = sandbox.cloneUncaughtException(QString("(preflight %1)").arg(entityID.toString()));
1769+ sandbox.clearExceptions();
1770+ } else if (testConstructor.isError()) {
1771+ exception = testConstructor;
1772+ }
1773+ }
15061774
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);
15071780 return;
15081781 }
15091782
1783+ // CONSTRUCTOR VIABILITY
15101784 if (!testConstructor.isFunction()) {
15111785 QString testConstructorType = QString(testConstructor.toVariant().typeName());
15121786 if (testConstructorType == "") {
@@ -1517,39 +1791,53 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co · 4 unmodified lines
15171791 if (testConstructorValue.size() > maxTestConstructorValueSize) {
15181792 testConstructorValue = testConstructorValue.mid(0, maxTestConstructorValueSize) + "...";
15191793 }
1520− scriptErrorMessage("Error -- ScriptEngine::loadEntityScript() entity:" + entityID.toString()
1521− + "failed to load entity script -- expected a function, got " + testConstructorType
1522− + "," + testConstructorValue
1523− + "," + scriptOrURL);
1794+ auto message = QString("failed to load entity script -- expected a function, got %1, %2")
1795+ .arg(testConstructorType).arg(testConstructorValue);
15241796
1525− if (!isFileUrl) {
1526− scriptCache->addScriptToBadScriptList(scriptOrURL);
1527− }
1528−
1529− newDetails.status = ERROR_RUNNING_SCRIPT;
1530− newDetails.errorInfo = "Could not find constructor";
1531− _entityScripts[entityID] = newDetails;
1797+ auto err = makeError(message);
1798+ err.setProperty("fileName", scriptOrURL);
1799+ err.setProperty("detail", "(constructor " + entityID.toString() + ")");
15321800
1801+ setError("Could not find constructor (" + testConstructorType + ")", EntityScriptStatus::ERROR_RUNNING_SCRIPT);
1802+ emit unhandledException(err);
15331803 return; // done processing script
15341804 }
15351805
1806+ // (this feeds into refreshFileScript)
15361807 int64_t lastModified = 0;
15371808 if (isFileUrl) {
15381809 QString file = QUrl(scriptOrURL).toLocalFile();
15391810 lastModified = (quint64)QFileInfo(file).lastModified().toMSecsSinceEpoch();
15401811 }
1812+
1813+ // THE ACTUAL EVALUATION AND CONSTRUCTION
15411814 QScriptValue entityScriptConstructor, entityScriptObject;
15421815 QUrl sandboxURL = currentSandboxURL.isEmpty() ? scriptOrURL : currentSandboxURL;
15431816 auto initialization = [&]{
15441817 entityScriptConstructor = evaluate(contents, fileName);
15451818 entityScriptObject = entityScriptConstructor.construct();
1819+
1820+ if (hasUncaughtException()) {
1821+ entityScriptObject = cloneUncaughtException("(construct " + entityID.toString() + ")");
1822+ clearExceptions();
1823+ }
15461824 };
1825+
15471826 doWithEnvironment(entityID, sandboxURL, initialization);
15481827
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;
15491837 newDetails.scriptObject = entityScriptObject;
15501838 newDetails.lastModified = lastModified;
15511839 newDetails.definingSandboxURL = sandboxURL;
1552− _entityScripts[entityID] = newDetails;
1840+ setEntityScriptDetails(entityID, newDetails);
15531841
15541842 if (isURL) {
15551843 setParentURL("");
@@ -1557,6 +1845,9 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co · 1 unmodified line
15571845
15581846 // if we got this far, then call the preload method
15591847 callEntityScriptMethod(entityID, "preload");
1848+
1849+ _occupiedScriptURLs.remove(entityScript);
1850+ processDeferredEntityLoads(entityScript, entityID);
15601851 }
15611852
15621853 void ScriptEngine::unloadEntityScript(const EntityItemID& entityID) {
@@ -1576,11 +1867,25 @@ void ScriptEngine::unloadEntityScript(const EntityItemID& entityID) { · 13 unmodified lines
15761867 #endif
15771868
15781869 if (_entityScripts.contains(entityID)) {
1579− if (_entityScripts[entityID].status == RUNNING) {
1870+ const EntityScriptDetails &oldDetails = _entityScripts[entityID];
1871+ if (isEntityScriptRunning(entityID)) {
15801872 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);
15811883 }
1582− _entityScripts.remove(entityID);
15831884 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+ }
15841889 }
15851890 }
15861891
@@ -1597,25 +1902,23 @@ void ScriptEngine::unloadAllEntityScripts() { · 10 unmodified lines
15971902 qCDebug(scriptengine) << "ScriptEngine::unloadAllEntityScripts() called on correct thread [" << thread() << "]";
15981903 #endif
15991904 foreach(const EntityItemID& entityID, _entityScripts.keys()) {
1600− if (_entityScripts[entityID].status == RUNNING) {
1601− callEntityScriptMethod(entityID, "unload");
1602− }
1905+ unloadEntityScript(entityID);
16031906 }
16041907 _entityScripts.clear();
1908+ emit entityScriptDetailsUpdated();
1909+ _occupiedScriptURLs.clear();
16051910
16061911 #ifdef DEBUG_ENGINE_STATE
1607− qCDebug(scriptengine) << "---- CURRENT STATE OF ENGINE: --------------------------";
1608− QScriptValueIterator it(globalObject());
1609− while (it.hasNext()) {
1610− it.next();
1611− qCDebug(scriptengine) << it.name() << ":" << it.value().toString();
1612− }
1613− qCDebug(scriptengine) << "--------------------------------------------------------";
1912+ _debugDump(
1913+ "---- CURRENT STATE OF ENGINE: --------------------------",
1914+ globalObject(),
1915+ "--------------------------------------------------------"
1916+ );
16141917 #endif // DEBUG_ENGINE_STATE
16151918 }
16161919
16171920 void ScriptEngine::refreshFileScript(const EntityItemID& entityID) {
1618− if (!_entityScripts.contains(entityID)) {
1921+ if (!HIFI_AUTOREFRESH_FILE_SCRIPTS || !_entityScripts.contains(entityID)) {
16191922 return;
16201923 }
16211924
@@ -1632,17 +1935,7 @@ void ScriptEngine::refreshFileScript(const EntityItemID& entityID) { · 10 unmodified lines
16321935 auto lastModified = QFileInfo(filePath).lastModified().toMSecsSinceEpoch();
16331936 if (lastModified > details.lastModified) {
16341937 scriptInfoMessage("Reloading modified script " + details.scriptText);
1635−
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− }
1938+ loadEntityScript(entityID, details.scriptText, true);
16461939 }
16471940 }
16481941 recurseGuard = false;
@@ -1666,11 +1959,14 @@ void ScriptEngine::doWithEnvironment(const EntityItemID& entityID, const QUrl& s · 17 unmodified lines
16661959 #else
16671960 operation();
16681961 #endif
1669− hadUncaughtExceptions(*this, _fileNameString, this);
1670−
1962+ if (!isEvaluating() && hasUncaughtException()) {
1963+ emit unhandledException(cloneUncaughtException(!entityID.isNull() ? entityID.toString() : __FUNCTION__));
1964+ clearExceptions();
1965+ }
16711966 currentEntityIdentifier = oldIdentifier;
16721967 currentSandboxURL = oldSandboxURL;
16731968 }
1969+
16741970 void ScriptEngine::callWithEnvironment(const EntityItemID& entityID, const QUrl& sandboxURL, QScriptValue function, QScriptValue thisObject, QScriptValueList args) {
16751971 auto operation = [&]() {
16761972 function.call(thisObject, args);
@@ -1696,8 +1992,10 @@ void ScriptEngine::callEntityScriptMethod(const EntityItemID& entityID, const QS · 19 unmodified lines
16961992 "entityID:" << entityID << "methodName:" << methodName;
16971993 #endif
16981994
1699− refreshFileScript(entityID);
1700− if (_entityScripts.contains(entityID) && _entityScripts[entityID].status == RUNNING) {
1995+ if (HIFI_AUTOREFRESH_FILE_SCRIPTS && methodName != "unload") {
1996+ refreshFileScript(entityID);
1997+ }
1998+ if (isEntityScriptRunning(entityID)) {
17011999 EntityScriptDetails details = _entityScripts[entityID];
17022000 QScriptValue entityScript = details.scriptObject; // previously loaded
17032001 if (entityScript.property(methodName).isFunction()) {
@@ -1728,8 +2026,10 @@ void ScriptEngine::callEntityScriptMethod(const EntityItemID& entityID, const QS · 24 unmodified lines
17282026 "entityID:" << entityID << "methodName:" << methodName << "event: pointerEvent";
17292027 #endif
17302028
1731− refreshFileScript(entityID);
1732− if (_entityScripts.contains(entityID) && _entityScripts[entityID].status == RUNNING) {
2029+ if (HIFI_AUTOREFRESH_FILE_SCRIPTS) {
2030+ refreshFileScript(entityID);
2031+ }
2032+ if (isEntityScriptRunning(entityID)) {
17332033 EntityScriptDetails details = _entityScripts[entityID];
17342034 QScriptValue entityScript = details.scriptObject; // previously loaded
17352035 if (entityScript.property(methodName).isFunction()) {
@@ -1741,7 +2041,6 @@ void ScriptEngine::callEntityScriptMethod(const EntityItemID& entityID, const QS · 5 unmodified lines
17412041 }
17422042 }
17432043
1744−
17452044 void ScriptEngine::callEntityScriptMethod(const EntityItemID& entityID, const QString& methodName, const EntityItemID& otherID, const Collision& collision) {
17462045 if (QThread::currentThread() != thread()) {
17472046 #ifdef THREAD_DEBUGGING
@@ -1761,8 +2060,10 @@ void ScriptEngine::callEntityScriptMethod(const EntityItemID& entityID, const QS · 13 unmodified lines
17612060 "entityID:" << entityID << "methodName:" << methodName << "otherID:" << otherID << "collision: collision";
17622061 #endif
17632062
1764− refreshFileScript(entityID);
1765− if (_entityScripts.contains(entityID) && _entityScripts[entityID].status == RUNNING) {
2063+ if (HIFI_AUTOREFRESH_FILE_SCRIPTS) {
2064+ refreshFileScript(entityID);
2065+ }
2066+ if (isEntityScriptRunning(entityID)) {
17662067 EntityScriptDetails details = _entityScripts[entityID];
17672068 QScriptValue entityScript = details.scriptObject; // previously loaded
17682069 if (entityScript.property(methodName).isFunction()) {
@@ -1774,3 +2075,4 @@ void ScriptEngine::callEntityScriptMethod(const EntityItemID& entityID, const QS · 5 unmodified lines
17742075 }
17752076 }
17762077 }
2078+