History

Merge pull request #9685 from humbletim/21114-part2

CR-2 21114 -- BaseScriptEngine.cpp, order-of-operations fixes, reworked JS Exceptions

Ryan Huffman committed 9 years ago

Commit a744e0d2 parents 2b11740 2505a8927 files · +1124 −238Browse files

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

Unified · Split

libraries/script-engine/src/ScriptEngine.cpp+389−204
@@ -50,6 +50,7 @@ · 49 unmodified lines
50 50
51#include "ArrayBufferViewClass.h"51#include "ArrayBufferViewClass.h"
52#include "BatchLoader.h"52#include "BatchLoader.h"
53#include "BaseScriptEngine.h"
53#include "DataViewClass.h"54#include "DataViewClass.h"
54#include "EventTypes.h"55#include "EventTypes.h"
55#include "FileScriptingInterface.h" // unzip project56#include "FileScriptingInterface.h" // unzip project
@@ -69,9 +70,12 @@ · 13 unmodified lines
69 70
70#include "MIDIEvent.h"71#include "MIDIEvent.h"
71 72
72const QString BaseScriptEngine::SCRIPT_EXCEPTION_FORMAT { "[UncaughtException] %1 in %2:%3" };
73static const QScriptEngine::QObjectWrapOptions DEFAULT_QOBJECT_WRAP_OPTIONS =73static const QScriptEngine::QObjectWrapOptions DEFAULT_QOBJECT_WRAP_OPTIONS =
74 QScriptEngine::ExcludeDeleteLater | QScriptEngine::ExcludeChildObjects;74 QScriptEngine::ExcludeDeleteLater | QScriptEngine::ExcludeChildObjects;
75static const QScriptValue::PropertyFlags READONLY_PROP_FLAGS { QScriptValue::ReadOnly | QScriptValue::Undeletable };
76static const QScriptValue::PropertyFlags READONLY_HIDDEN_PROP_FLAGS { READONLY_PROP_FLAGS | QScriptValue::SkipInEnumeration };
77
78
75 79
76static const bool HIFI_AUTOREFRESH_FILE_SCRIPTS { true };80static const bool HIFI_AUTOREFRESH_FILE_SCRIPTS { true };
77 81
@@ -90,13 +94,9 @@ static QScriptValue debugPrint(QScriptContext* context, QScriptEngine* engine){ · 12 unmodified lines
90 }94 }
91 qCDebug(scriptengineScript).noquote() << "script:print()<<" << message; // noquote() so that \n is treated as newline95 qCDebug(scriptengineScript).noquote() << "script:print()<<" << message; // noquote() so that \n is treated as newline
92 96
93 message = message.replace("\\", "\\\\")97 // FIXME - this approach neeeds revisiting. print() comes here, which ends up calling Script.print?
94 .replace("\n", "\\n")98 engine->globalObject().property("Script").property("print")
95 .replace("\r", "\\r")99 .call(engine->nullValue(), QScriptValueList({ message }));
96 .replace("'", "\\'");
97
98 // FIXME - this approach neeeds revisiting. print() comes here, which ends up doing an evaluate?
99 engine->evaluate("Script.print('" + message + "')");
100 100
101 return QScriptValue();101 return QScriptValue();
102}102}
@@ -140,52 +140,15 @@ QString encodeEntityIdIntoEntityUrl(const QString& url, const QString& entityID) · 37 unmodified lines
140 return url + " [EntityID:" + entityID + "]";140 return url + " [EntityID:" + entityID + "]";
141}141}
142 142
143QString BaseScriptEngine::lintScript(const QString& sourceCode, const QString& fileName, const int lineNumber) {143QString ScriptEngine::logException(const QScriptValue& exception) {
144 const auto syntaxCheck = checkSyntax(sourceCode);144 auto message = formatException(exception);
145 if (syntaxCheck.state() != syntaxCheck.Valid) {
146 const auto error = syntaxCheck.errorMessage();
147 const auto line = QString::number(syntaxCheck.errorLineNumber());
148 const auto column = QString::number(syntaxCheck.errorColumnNumber());
149 const auto message = QString("[SyntaxError] %1 in %2:%3(%4)").arg(error, fileName, line, column);
150 return message;
151 }
152 return QString();
153}
154
155QString BaseScriptEngine::formatUncaughtException(const QString& overrideFileName) {
156 QString message;
157 if (hasUncaughtException()) {
158 const auto error = uncaughtException();
159 const auto backtrace = uncaughtExceptionBacktrace();
160 const auto exception = error.toString();
161 auto filename = overrideFileName;
162 if (filename.isEmpty()) {
163 QScriptContextInfo ctx { currentContext() };
164 filename = ctx.fileName();
165 }
166 const auto line = QString::number(uncaughtExceptionLineNumber());
167
168 message = QString(SCRIPT_EXCEPTION_FORMAT).arg(exception, overrideFileName, line);
169 if (!backtrace.empty()) {
170 static const auto lineSeparator = "\n ";
171 message += QString("\n[Backtrace]%1%2").arg(lineSeparator, backtrace.join(lineSeparator));
172 }
173 }
174 return message;
175}
176
177QString ScriptEngine::reportUncaughtException(const QString& overrideFileName) {
178 QString message;
179 if (!hasUncaughtException()) {
180 return message;
181 }
182 message = formatUncaughtException(overrideFileName.isEmpty() ? _fileNameString : overrideFileName);
183 scriptErrorMessage(qPrintable(message));145 scriptErrorMessage(qPrintable(message));
184 return message;146 return message;
185}147}
186 148
187int ScriptEngine::processLevelMaxRetries { ScriptRequest::MAX_RETRIES };149int ScriptEngine::processLevelMaxRetries { ScriptRequest::MAX_RETRIES };
188ScriptEngine::ScriptEngine(Context context, const QString& scriptContents, const QString& fileNameString) :150ScriptEngine::ScriptEngine(Context context, const QString& scriptContents, const QString& fileNameString) :
151 BaseScriptEngine(),
189 _context(context),152 _context(context),
190 _scriptContents(scriptContents),153 _scriptContents(scriptContents),
191 _timerFunctionMap(),154 _timerFunctionMap(),
@@ -195,16 +158,30 @@ ScriptEngine::ScriptEngine(Context context, const QString& scriptContents, const · 3 unmodified lines
195 DependencyManager::get<ScriptEngines>()->addScriptEngine(this);158 DependencyManager::get<ScriptEngines>()->addScriptEngine(this);
196 159
197 connect(this, &QScriptEngine::signalHandlerException, this, [this](const QScriptValue& exception) {160 connect(this, &QScriptEngine::signalHandlerException, this, [this](const QScriptValue& exception) {
198 reportUncaughtException();161 if (hasUncaughtException()) {
199 clearExceptions();162 // the engine's uncaughtException() seems to produce much better stack traces here
200 });163 emit unhandledException(cloneUncaughtException("signalHandlerException"));
164 clearExceptions();
165 } else {
166 // ... but may not always be available -- so if needed we fallback to the passed exception
167 emit unhandledException(exception);
168 }
169 }, Qt::DirectConnection);
201 170
202 setProcessEventsInterval(MSECS_PER_SECOND);171 setProcessEventsInterval(MSECS_PER_SECOND);
203 if (isEntityServerScript()) {172 if (isEntityServerScript()) {
204 qCDebug(scriptengine) << "isEntityServerScript() -- limiting maxRetries to 1";173 qCDebug(scriptengine) << "isEntityServerScript() -- limiting maxRetries to 1";
205 processLevelMaxRetries = 1;174 processLevelMaxRetries = 1;
206 }175 }
207 qCDebug(scriptengine) << getContext() << "processLevelMaxRetries =" << processLevelMaxRetries;176
177 // this is where all unhandled exceptions end up getting logged
178 connect(this, &BaseScriptEngine::unhandledException, this, [this](const QScriptValue& err) {
179 auto output = err.engine() == this ? err : makeError(err);
180 if (!output.property("detail").isValid()) {
181 output.setProperty("detail", "UnhandledException");
182 }
183 logException(output);
184 });
208}185}
209 186
210QString ScriptEngine::getContext() const {187QString ScriptEngine::getContext() const {
@@ -224,13 +201,22 @@ QString ScriptEngine::getContext() const { · 13 unmodified lines
224}201}
225 202
226ScriptEngine::~ScriptEngine() {203ScriptEngine::~ScriptEngine() {
204 // FIXME: are these scriptInfoMessage/scriptWarningMessage segfaulting anybody else at app shutdown?
205#if !defined(Q_OS_LINUX)
227 scriptInfoMessage("Script Engine shutting down:" + getFilename());206 scriptInfoMessage("Script Engine shutting down:" + getFilename());
207#else
208 qCDebug(scriptengine) << "~ScriptEngine()" << this;
209#endif
228 210
229 auto scriptEngines = DependencyManager::get<ScriptEngines>();211 auto scriptEngines = DependencyManager::get<ScriptEngines>();
230 if (scriptEngines) {212 if (scriptEngines) {
231 scriptEngines->removeScriptEngine(this);213 scriptEngines->removeScriptEngine(this);
232 } else {214 } else {
215#if !defined(Q_OS_LINUX)
233 scriptWarningMessage("Script destroyed after ScriptEngines!");216 scriptWarningMessage("Script destroyed after ScriptEngines!");
217#else
218 qCWarning(scriptengine) << ("Script destroyed after ScriptEngines!");
219#endif
234 }220 }
235}221}
236 222
@@ -320,9 +306,12 @@ void ScriptEngine::runDebuggable() { · 83 unmodified lines
320 }306 }
321 }307 }
322 _lastUpdate = now;308 _lastUpdate = now;
323 // Debug and clear exceptions309
324 if (hasUncaughtException()) {310 // only clear exceptions if we are not in the middle of evaluating
325 reportUncaughtException();311 if (!isEvaluating() && hasUncaughtException()) {
312 qCWarning(scriptengine) << __FUNCTION__ << "---------- UNCAUGHT EXCEPTION --------";
313 qCWarning(scriptengine) << "runDebuggable" << uncaughtException().toString();
314 logException(__FUNCTION__);
326 clearExceptions();315 clearExceptions();
327 }316 }
328 });317 });
@@ -357,10 +346,9 @@ void ScriptEngine::runInThread() { · 28 unmodified lines
357 workerThread->start();346 workerThread->start();
358}347}
359 348
360void ScriptEngine::executeOnScriptThread(std::function<void()> function, bool blocking ) {349void ScriptEngine::executeOnScriptThread(std::function<void()> function, const Qt::ConnectionType& type ) {
361 if (QThread::currentThread() != thread()) {350 if (QThread::currentThread() != thread()) {
362 QMetaObject::invokeMethod(this, "executeOnScriptThread", blocking ? Qt::BlockingQueuedConnection : Qt::QueuedConnection,351 QMetaObject::invokeMethod(this, "executeOnScriptThread", type, Q_ARG(std::function<void()>, function));
363 Q_ARG(std::function<void()>, function));
364 return;352 return;
365 }353 }
366 354
@@ -559,6 +547,7 @@ void ScriptEngine::init() { · 192 unmodified lines
559 qCWarning(scriptengine) << "deletingEntity while entity script is still running!" << entityID;547 qCWarning(scriptengine) << "deletingEntity while entity script is still running!" << entityID;
560 }548 }
561 _entityScripts.remove(entityID);549 _entityScripts.remove(entityID);
550 emit entityScriptDetailsUpdated();
562 }551 }
563 });552 });
564 553
@@ -591,8 +580,7 @@ void ScriptEngine::init() { · 26 unmodified lines
591 QScriptValue webSocketConstructorValue = newFunction(WebSocketClass::constructor);580 QScriptValue webSocketConstructorValue = newFunction(WebSocketClass::constructor);
592 globalObject().setProperty("WebSocket", webSocketConstructorValue);581 globalObject().setProperty("WebSocket", webSocketConstructorValue);
593 582
594 QScriptValue printConstructorValue = newFunction(debugPrint);583 globalObject().setProperty("print", newFunction(debugPrint));
595 globalObject().setProperty("print", printConstructorValue);
596 584
597 QScriptValue audioEffectOptionsConstructorValue = newFunction(AudioEffectOptions::constructor);585 QScriptValue audioEffectOptionsConstructorValue = newFunction(AudioEffectOptions::constructor);
598 globalObject().setProperty("AudioEffectOptions", audioEffectOptionsConstructorValue);586 globalObject().setProperty("AudioEffectOptions", audioEffectOptionsConstructorValue);
@@ -606,6 +594,7 @@ void ScriptEngine::init() { · 7 unmodified lines
606 qScriptRegisterMetaType(this, wscReadyStateToScriptValue, wscReadyStateFromScriptValue);594 qScriptRegisterMetaType(this, wscReadyStateToScriptValue, wscReadyStateFromScriptValue);
607 595
608 registerGlobalObject("Script", this);596 registerGlobalObject("Script", this);
597
609 registerGlobalObject("Audio", &AudioScriptingInterface::getInstance());598 registerGlobalObject("Audio", &AudioScriptingInterface::getInstance());
610 registerGlobalObject("Entities", entityScriptingInterface.data());599 registerGlobalObject("Entities", entityScriptingInterface.data());
611 registerGlobalObject("Quat", &_quatLibrary);600 registerGlobalObject("Quat", &_quatLibrary);
@@ -874,7 +863,6 @@ void ScriptEngine::addEventHandler(const EntityItemID& entityID, const QString& · 262 unmodified lines
874 handlersForEvent << handlerData; // Note that the same handler can be added many times. See removeEntityEventHandler().863 handlersForEvent << handlerData; // Note that the same handler can be added many times. See removeEntityEventHandler().
875}864}
876 865
877
878QScriptValue ScriptEngine::evaluate(const QString& sourceCode, const QString& fileName, int lineNumber) {866QScriptValue ScriptEngine::evaluate(const QString& sourceCode, const QString& fileName, int lineNumber) {
879 if (DependencyManager::get<ScriptEngines>()->isStopped()) {867 if (DependencyManager::get<ScriptEngines>()->isStopped()) {
880 return QScriptValue(); // bail early868 return QScriptValue(); // bail early
@@ -896,23 +884,30 @@ QScriptValue ScriptEngine::evaluate(const QString& sourceCode, const QString& fi · 15 unmodified lines
896 884
897 // Check syntax885 // Check syntax
898 auto syntaxError = lintScript(sourceCode, fileName);886 auto syntaxError = lintScript(sourceCode, fileName);
887 if (syntaxError.isError()) {
888 if (isEvaluating()) {
889 currentContext()->throwValue(syntaxError);
890 } else {
891 syntaxError.setProperty("detail", "evaluate");
892 emit unhandledException(syntaxError);
893 }
894 return syntaxError;
895 }
899 QScriptProgram program { sourceCode, fileName, lineNumber };896 QScriptProgram program { sourceCode, fileName, lineNumber };
900 if (!syntaxError.isEmpty() || program.isNull()) {897 if (program.isNull()) {
901 scriptErrorMessage(qPrintable(syntaxError));898 // can this happen?
902 return QScriptValue();899 auto err = makeError("could not create QScriptProgram for " + fileName);
900 emit unhandledException(err);
901 return err;
903 }902 }
904 903
905 ++_evaluatesPending;904 QScriptValue result;
906 auto result = BaseScriptEngine::evaluate(program);905 {
907 --_evaluatesPending;906 result = BaseScriptEngine::evaluate(program);
908 907 if (!isEvaluating() && hasUncaughtException()) {
909 if (hasUncaughtException()) {908 emit unhandledException(cloneUncaughtException(__FUNCTION__));
910 result = uncaughtException();909 clearExceptions();
911 reportUncaughtException(program.fileName());910 }
912 emit evaluationFinished(result, true);
913 clearExceptions();
914 } else {
915 emit evaluationFinished(result, false);
916 }911 }
917 return result;912 return result;
918}913}
@@ -933,8 +928,13 @@ void ScriptEngine::run() { · 14 unmodified lines
933 _isRunning = true;928 _isRunning = true;
934 emit runningStateChanged();929 emit runningStateChanged();
935 930
936 QScriptValue result = evaluate(_scriptContents, _fileNameString);931 {
937 932 evaluate(_scriptContents, _fileNameString);
933 if (!isEvaluating() && hasUncaughtException()) {
934 emit unhandledException(cloneUncaughtException(__FUNCTION__));
935 clearExceptions();
936 }
937 }
938#ifdef _WIN32938#ifdef _WIN32
939 // VS13 does not sleep_until unless it uses the system_clock, see:939 // VS13 does not sleep_until unless it uses the system_clock, see:
940 // https://www.reddit.com/r/cpp_questions/comments/3o71ic/sleep_until_not_working_with_a_time_pointsteady/940 // https://www.reddit.com/r/cpp_questions/comments/3o71ic/sleep_until_not_working_with_a_time_pointsteady/
@@ -1061,13 +1061,14 @@ void ScriptEngine::run() { · 120 unmodified lines
1061 }1061 }
1062 _lastUpdate = now;1062 _lastUpdate = now;
1063 1063
1064 // Debug and clear exceptions1064 // only clear exceptions if we are not in the middle of evaluating
1065 if (hasUncaughtException()) {1065 if (!isEvaluating() && hasUncaughtException()) {
1066 reportUncaughtException();1066 qCWarning(scriptengine) << __FUNCTION__ << "---------- UNCAUGHT EXCEPTION --------";
1067 qCWarning(scriptengine) << "runInThread" << uncaughtException().toString();
1068 emit unhandledException(cloneUncaughtException(__FUNCTION__));
1067 clearExceptions();1069 clearExceptions();
1068 }1070 }
1069 }1071 }
1070
1071 scriptInfoMessage("Script Engine stopping:" + getFilename());1072 scriptInfoMessage("Script Engine stopping:" + getFilename());
1072 1073
1073 stopAllTimers(); // make sure all our timers are stopped if the script is ending1074 stopAllTimers(); // make sure all our timers are stopped if the script is ending
@@ -1100,9 +1101,11 @@ void ScriptEngine::run() { · 26 unmodified lines
1100// we want to only call it in our own run "shutdown" processing.1101// we want to only call it in our own run "shutdown" processing.
1101void ScriptEngine::stopAllTimers() {1102void ScriptEngine::stopAllTimers() {
1102 QMutableHashIterator<QTimer*, CallbackData> i(_timerFunctionMap);1103 QMutableHashIterator<QTimer*, CallbackData> i(_timerFunctionMap);
1104 int j {0};
1103 while (i.hasNext()) {1105 while (i.hasNext()) {
1104 i.next();1106 i.next();
1105 QTimer* timer = i.key();1107 QTimer* timer = i.key();
1108 qCDebug(scriptengine) << getFilename() << "stopAllTimers[" << j++ << "]";
1106 stopTimer(timer);1109 stopTimer(timer);
1107 }1110 }
1108}1111}
@@ -1197,11 +1200,11 @@ void ScriptEngine::timerFired() { · 88 unmodified lines
1197 auto postTimer = p_high_resolution_clock::now();1200 auto postTimer = p_high_resolution_clock::now();
1198 auto elapsed = (postTimer - preTimer);1201 auto elapsed = (postTimer - preTimer);
1199 _totalTimerExecution += std::chrono::duration_cast<std::chrono::microseconds>(elapsed);1202 _totalTimerExecution += std::chrono::duration_cast<std::chrono::microseconds>(elapsed);
1200 1203 } else {
1204 qCWarning(scriptengine) << "timerFired -- invalid function" << timerData.function.toVariant().toString();
1201 }1205 }
1202}1206}
1203 1207
1204
1205QObject* ScriptEngine::setupTimerWithInterval(const QScriptValue& function, int intervalMS, bool isSingleShot) {1208QObject* ScriptEngine::setupTimerWithInterval(const QScriptValue& function, int intervalMS, bool isSingleShot) {
1206 // create the timer, add it to the map, and start it1209 // create the timer, add it to the map, and start it
1207 QTimer* newTimer = new QTimer(this);1210 QTimer* newTimer = new QTimer(this);
@@ -1218,7 +1221,7 @@ QObject* ScriptEngine::setupTimerWithInterval(const QScriptValue& function, int · 10 unmodified lines
1218 // make sure the timer stops when the script does1221 // make sure the timer stops when the script does
1219 connect(this, &ScriptEngine::scriptEnding, newTimer, &QTimer::stop);1222 connect(this, &ScriptEngine::scriptEnding, newTimer, &QTimer::stop);
1220 1223
1221 CallbackData timerData = {function, currentEntityIdentifier, currentSandboxURL };1224 CallbackData timerData = { function, currentEntityIdentifier, currentSandboxURL };
1222 _timerFunctionMap.insert(newTimer, timerData);1225 _timerFunctionMap.insert(newTimer, timerData);
1223 1226
1224 newTimer->start(intervalMS);1227 newTimer->start(intervalMS);
@@ -1248,35 +1251,46 @@ void ScriptEngine::stopTimer(QTimer *timer) { · 23 unmodified lines
1248 timer->stop();1251 timer->stop();
1249 _timerFunctionMap.remove(timer);1252 _timerFunctionMap.remove(timer);
1250 delete timer;1253 delete timer;
1254 } else {
1255 qCDebug(scriptengine) << "stopTimer -- not in _timerFunctionMap" << timer;
1251 }1256 }
1252}1257}
1253 1258
1254QUrl ScriptEngine::resolvePath(const QString& include) const {1259QUrl ScriptEngine::resolvePath(const QString& include) const {
1255 QUrl url(include);1260 QUrl url(include);
1256 // first lets check to see if it's already a full URL1261 // first lets check to see if it's already a full URL -- or a Windows path like "c:/"
1257 if (!url.scheme().isEmpty()) {1262 if (include.startsWith("/") || url.scheme().length() == 1) {
1263 url = QUrl::fromLocalFile(include);
1264 }
1265 if (!url.isRelative()) {
1258 return expandScriptUrl(url);1266 return expandScriptUrl(url);
1259 }1267 }
1260 1268
1261 QScriptContextInfo contextInfo { currentContext()->parentContext() };
1262
1263
1264 // we apparently weren't a fully qualified url, so, let's assume we're relative1269 // we apparently weren't a fully qualified url, so, let's assume we're relative
1265 // to the original URL of our script1270 // to the first absolute URL in the JS scope chain
1266 QUrl parentURL = contextInfo.fileName();1271 QUrl parentURL;
1267 if (parentURL.isEmpty()) {1272 auto context = currentContext();
1268 if (_parentURL.isEmpty()) {1273 do {
1269 parentURL = QUrl(_fileNameString);1274 QScriptContextInfo contextInfo { context };
1270 } else {1275 parentURL = QUrl(contextInfo.fileName());
1271 parentURL = QUrl(_parentURL);1276 context = context->parentContext();
1277 } while (parentURL.isRelative() && context);
1278
1279 if (parentURL.isRelative()) {
1280 // fallback to the "include" parent (if defined, this will already be absolute)
1281 parentURL = QUrl(_parentURL);
1282 }
1283
1284 if (parentURL.isRelative()) {
1285 // fallback to the original script engine URL
1286 parentURL = QUrl(_fileNameString);
1287
1288 // if still relative and path-like, then this is probably a local file...
1289 if (parentURL.isRelative() && url.path().contains("/")) {
1290 parentURL = QUrl::fromLocalFile(_fileNameString);
1272 }1291 }
1273 }1292 }
1274 1293
1275 // if the parent URL's scheme is empty, then this is probably a local file...
1276 if (parentURL.scheme().isEmpty()) {
1277 parentURL = QUrl::fromLocalFile(_fileNameString);
1278 }
1279
1280 // at this point we should have a legitimate fully qualified URL for our parent1294 // at this point we should have a legitimate fully qualified URL for our parent
1281 url = expandScriptUrl(parentURL.resolved(url));1295 url = expandScriptUrl(parentURL.resolved(url));
1282 return url;1296 return url;
@@ -1301,22 +1315,7 @@ void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callbac · 18 unmodified lines
1301 return; // bail early1315 return; // bail early
1302 }1316 }
1303 QList<QUrl> urls;1317 QList<QUrl> urls;
1304 bool knowsSensitivity = false;
1305 Qt::CaseSensitivity sensitivity { Qt::CaseSensitive };
1306 auto getSensitivity = [&]() {
1307 if (!knowsSensitivity) {
1308 QString path = currentSandboxURL.path();
1309 QFileInfo upperFI(path.toUpper());
1310 QFileInfo lowerFI(path.toLower());
1311 sensitivity = (upperFI == lowerFI) ? Qt::CaseInsensitive : Qt::CaseSensitive;
1312 knowsSensitivity = true;
1313 }
1314 return sensitivity;
1315 };
1316 1318
1317 // Guard against meaningless query and fragment parts.
1318 // Do NOT use PreferLocalFile as its behavior is unpredictable (e.g., on defaultScriptsLocation())
1319 const auto strippingFlags = QUrl::RemoveFilename | QUrl::RemoveQuery | QUrl::RemoveFragment;
1320 for (QString includeFile : includeFiles) {1319 for (QString includeFile : includeFiles) {
1321 QString file = ResourceManager::normalizeURL(includeFile);1320 QString file = ResourceManager::normalizeURL(includeFile);
1322 QUrl thisURL;1321 QUrl thisURL;
@@ -1333,10 +1332,8 @@ void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callbac · 10 unmodified lines
1333 thisURL = resolvePath(file);1332 thisURL = resolvePath(file);
1334 }1333 }
1335 1334
1336 if (!isStandardLibrary && !currentSandboxURL.isEmpty() && (thisURL.scheme() == "file") &&1335 bool disallowOutsideFiles = thisURL.isLocalFile() && !isStandardLibrary && !currentSandboxURL.isLocalFile();
1337 (currentSandboxURL.scheme() != "file" ||1336 if (disallowOutsideFiles && !PathUtils::isDescendantOf(thisURL, currentSandboxURL)) {
1338 !thisURL.toString(strippingFlags).startsWith(currentSandboxURL.toString(strippingFlags), getSensitivity()))) {
1339
1340 scriptWarningMessage("Script.include() ignoring file path" + thisURL.toString() 1337 scriptWarningMessage("Script.include() ignoring file path" + thisURL.toString()
1341 + "outside of original entity script" + currentSandboxURL.toString());1338 + "outside of original entity script" + currentSandboxURL.toString());
1342 } else {1339 } else {
@@ -1373,6 +1370,10 @@ void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callbac · 30 unmodified lines
1373 };1370 };
1374 1371
1375 doWithEnvironment(capturedEntityIdentifier, capturedSandboxURL, operation);1372 doWithEnvironment(capturedEntityIdentifier, capturedSandboxURL, operation);
1373 if (hasUncaughtException()) {
1374 emit unhandledException(cloneUncaughtException(__FUNCTION__));
1375 clearExceptions();
1376 }
1376 } else {1377 } else {
1377 scriptWarningMessage("Script.include() skipping evaluation of previously included url:" + url.toString());1378 scriptWarningMessage("Script.include() skipping evaluation of previously included url:" + url.toString());
1378 }1379 }
@@ -1474,10 +1475,16 @@ int ScriptEngine::getNumRunningEntityScripts() const { · 95 unmodified lines
1474 return sum;1475 return sum;
1475}1476}
1476 1477
1477QString ScriptEngine::getEntityScriptStatus(const EntityItemID& entityID) {1478void ScriptEngine::setEntityScriptDetails(const EntityItemID& entityID, const EntityScriptDetails& details) {
1478 if (_entityScripts.contains(entityID))1479 _entityScripts[entityID] = details;
1479 return EntityScriptStatus_::valueToKey(_entityScripts[entityID].status).toLower();1480 emit entityScriptDetailsUpdated();
1480 return QString();1481}
1482
1483void ScriptEngine::updateEntityScriptStatus(const EntityItemID& entityID, const EntityScriptStatus &status, const QString& errorInfo) {
1484 EntityScriptDetails &details = _entityScripts[entityID];
1485 details.status = status;
1486 details.errorInfo = errorInfo;
1487 emit entityScriptDetailsUpdated();
1481}1488}
1482 1489
1483bool ScriptEngine::getEntityScriptDetails(const EntityItemID& entityID, EntityScriptDetails &details) const {1490bool ScriptEngine::getEntityScriptDetails(const EntityItemID& entityID, EntityScriptDetails &details) const {
@@ -1489,43 +1496,166 @@ bool ScriptEngine::getEntityScriptDetails(const EntityItemID& entityID, EntitySc · 5 unmodified lines
1489 return true;1496 return true;
1490}1497}
1491 1498
1492void ScriptEngine::setEntityScriptDetails(const EntityItemID& entityID, const EntityScriptDetails& details) {1499const static EntityItemID BAD_SCRIPT_UUID_PLACEHOLDER { "{20170224-dead-face-0000-cee000021114}" };
1493 _entityScripts[entityID] = details;
1494 emit entityScriptDetailsUpdated();
1495}
1496 1500
1497void ScriptEngine::updateEntityScriptStatus(const EntityItemID& entityID, const EntityScriptStatus &status, const QString& errorInfo) {1501void ScriptEngine::processDeferredEntityLoads(const QString& entityScript, const EntityItemID& leaderID) {
1498 EntityScriptDetails &details = _entityScripts[entityID];1502 QList<DeferredLoadEntity> retryLoads;
1499 details.status = status;1503 QMutableListIterator<DeferredLoadEntity> i(_deferredEntityLoads);
1500 details.errorInfo = errorInfo;1504 while (i.hasNext()) {
1501 emit entityScriptDetailsUpdated();1505 auto retry = i.next();
1502}1506 if (retry.entityScript == entityScript) {
1507 retryLoads << retry;
1508 i.remove();
1509 }
1510 }
1511 foreach(DeferredLoadEntity retry, retryLoads) {
1512 // check whether entity was since been deleted
1513 if (!_entityScripts.contains(retry.entityID)) {
1514 qCDebug(scriptengine) << "processDeferredEntityLoads -- entity details gone (entity deleted?)"
1515 << retry.entityID;
1516 continue;
1517 }
1503 1518
1504// since all of these operations can be asynch we will always do the actual work in the response handler1519 // check whether entity has since been unloaded or otherwise errored-out
1505// for the download1520 auto details = _entityScripts[retry.entityID];
1506void ScriptEngine::loadEntityScript(QWeakPointer<ScriptEngine> theEngine, const EntityItemID& entityID, const QString& entityScript, bool forceRedownload) {1521 if (details.status != EntityScriptStatus::PENDING) {
1507 auto engine = theEngine.data();1522 qCDebug(scriptengine) << "processDeferredEntityLoads -- entity status no longer PENDING; "
1508 engine->executeOnScriptThread([=]{1523 << retry.entityID << details.status;
1509 EntityScriptDetails details = engine->_entityScripts[entityID];1524 continue;
1510 if (details.status == EntityScriptStatus::PENDING || details.status == EntityScriptStatus::UNLOADED) {
1511 engine->updateEntityScriptStatus(entityID, EntityScriptStatus::LOADING, QThread::currentThread()->objectName());
1512 }1525 }
1513 });
1514 1526
1515 // NOTE: If the script content is not currently in the cache, the LAMBDA here will be called on the Main Thread1527 // propagate leader's failure reasons to the pending entity
1516 // which means we're guaranteed that it's not the correct thread for the ScriptEngine. This means1528 const auto leaderDetails = _entityScripts[leaderID];
1517 // when we get into entityScriptContentAvailable() we will likely invokeMethod() to get it over1529 if (leaderDetails.status != EntityScriptStatus::RUNNING) {
1518 // to the "Entities" ScriptEngine thread.1530 qCDebug(scriptengine) << QString("... pending load of %1 cancelled (leader: %2 status: %3)")
1519 DependencyManager::get<ScriptCache>()->getScriptContents(entityScript, [theEngine, entityID](const QString& scriptOrURL, const QString& contents, bool isURL, bool success, const QString &status) {1531 .arg(retry.entityID.toString()).arg(leaderID.toString()).arg(leaderDetails.status);
1520 QSharedPointer<ScriptEngine> strongEngine = theEngine.toStrongRef();1532
1521 if (strongEngine) {1533 auto extraDetail = QString("\n(propagated from %1)").arg(leaderID.toString());
1522#ifdef THREAD_DEBUGGING1534 if (leaderDetails.status == EntityScriptStatus::ERROR_LOADING_SCRIPT ||
1523 qCDebug(scriptengine) << "ScriptEngine::entityScriptContentAvailable() IN LAMBDA contentAvailable on thread ["1535 leaderDetails.status == EntityScriptStatus::ERROR_RUNNING_SCRIPT) {
1524 << QThread::currentThread() << "] expected thread [" << strongEngine->thread() << "]";1536 // propagate same error so User doesn't have to hunt down stampede's leader
1537 updateEntityScriptStatus(retry.entityID, leaderDetails.status, leaderDetails.errorInfo + extraDetail);
1538 } else {
1539 // the leader Entity somehow ended up in some other state (rapid-fire delete or unload could cause)
1540 updateEntityScriptStatus(retry.entityID, EntityScriptStatus::ERROR_LOADING_SCRIPT,
1541 "A previous Entity failed to load using this script URL; reload to try again." + extraDetail);
1542 }
1543 continue;
1544 }
1545
1546 if (_occupiedScriptURLs.contains(retry.entityScript)) {
1547 qCWarning(scriptengine) << "--- SHOULD NOT HAPPEN -- recursive call into processDeferredEntityLoads" << retry.entityScript;
1548 continue;
1549 }
1550
1551 // if we made it here then the leading entity was successful so proceed with normal load
1552 loadEntityScript(retry.entityID, retry.entityScript, false);
1553 }
1554}
1555
1556void ScriptEngine::loadEntityScript(const EntityItemID& entityID, const QString& entityScript, bool forceRedownload) {
1557 if (QThread::currentThread() != thread()) {
1558 QMetaObject::invokeMethod(this, "loadEntityScript",
1559 Q_ARG(const EntityItemID&, entityID),
1560 Q_ARG(const QString&, entityScript),
1561 Q_ARG(bool, forceRedownload)
1562 );
1563 return;
1564 }
1565 PROFILE_RANGE(script, __FUNCTION__);
1566
1567 if (isStopping() || DependencyManager::get<ScriptEngines>()->isStopped()) {
1568 qCDebug(scriptengine) << "loadEntityScript.start " << entityScript << entityID.toString()
1569 << " but isStopping==" << isStopping()
1570 << " || engines->isStopped==" << DependencyManager::get<ScriptEngines>()->isStopped();
1571 return;
1572 }
1573
1574 if (!_entityScripts.contains(entityID)) {
1575 // make sure EntityScriptDetails has an entry for this UUID right away
1576 // (which allows bailing from the loading/provisioning process early if the Entity gets deleted mid-flight)
1577 updateEntityScriptStatus(entityID, EntityScriptStatus::PENDING, "...pending...");
1578 }
1579
1580 // This "occupied" approach allows multiple Entities to boot from the same script URL while still taking
1581 // full advantage of cacheable require modules. This only affects Entities literally coming in back-to-back
1582 // before the first one has time to finish loading.
1583 if (_occupiedScriptURLs.contains(entityScript)) {
1584 auto currentEntityID = _occupiedScriptURLs[entityScript];
1585 if (currentEntityID == BAD_SCRIPT_UUID_PLACEHOLDER) {
1586 if (forceRedownload) {
1587 // script was previously marked unusable, but we're reloading so reset it
1588 _occupiedScriptURLs.remove(entityScript);
1589 } else {
1590 // since not reloading, assume that the exact same input would produce the exact same output again
1591 // note: this state gets reset with "reload all scripts," leaving/returning to a Domain, clear cache, etc.
1592#ifdef DEBUG_ENTITY_STATES
1593 qCDebug(scriptengine) << QString("loadEntityScript.cancelled entity: %1 script: %2 (previous script failure)")
1594 .arg(entityID.toString()).arg(entityScript);
1595#endif
1596 updateEntityScriptStatus(entityID, EntityScriptStatus::ERROR_LOADING_SCRIPT,
1597 "A previous Entity failed to load using this script URL; reload to try again.");
1598 return;
1599 }
1600 } else {
1601 // another entity is busy loading from this script URL so wait for them to finish
1602#ifdef DEBUG_ENTITY_STATES
1603 qCDebug(scriptengine) << QString("loadEntityScript.deferring[%0] entity: %1 script: %2 (waiting on %3)")
1604 .arg(_deferredEntityLoads.size()).arg(entityID.toString()).arg(entityScript).arg(currentEntityID.toString());
1525#endif1605#endif
1526 strongEngine->entityScriptContentAvailable(entityID, scriptOrURL, contents, isURL, success, status);1606 _deferredEntityLoads.push_back({ entityID, entityScript });
1607 return;
1527 }1608 }
1528 }, forceRedownload, processLevelMaxRetries);1609 }
1610
1611 // the scriptURL slot is available; flag as in-use
1612 _occupiedScriptURLs[entityScript] = entityID;
1613
1614#ifdef DEBUG_ENTITY_STATES
1615 auto previousStatus = _entityScripts.contains(entityID) ? _entityScripts[entityID].status : EntityScriptStatus::PENDING;
1616 qCDebug(scriptengine) << "loadEntityScript.LOADING: " << entityScript << entityID.toString()
1617 << "(previous: " << previousStatus << ")";
1618#endif
1619
1620 EntityScriptDetails newDetails;
1621 newDetails.scriptText = entityScript;
1622 newDetails.status = EntityScriptStatus::LOADING;
1623 newDetails.definingSandboxURL = currentSandboxURL;
1624 setEntityScriptDetails(entityID, newDetails);
1625
1626 auto scriptCache = DependencyManager::get<ScriptCache>();
1627 // note: see EntityTreeRenderer.cpp for shared pointer lifecycle management
1628 QWeakPointer<ScriptEngine> weakRef(sharedFromThis());
1629 scriptCache->getScriptContents(entityScript,
1630 [this, weakRef, entityScript, entityID](const QString& url, const QString& contents, bool isURL, bool success, const QString& status) {
1631 QSharedPointer<ScriptEngine> strongRef(weakRef);
1632 if (!strongRef) {
1633 qCWarning(scriptengine) << "loadEntityScript.contentAvailable -- ScriptEngine was deleted during getScriptContents!!";
1634 return;
1635 }
1636 if (isStopping()) {
1637#ifdef DEBUG_ENTITY_STATES
1638 qCDebug(scriptengine) << "loadEntityScript.contentAvailable -- stopping";
1639#endif
1640 return;
1641 }
1642 executeOnScriptThread([=]{
1643#ifdef DEBUG_ENTITY_STATES
1644 qCDebug(scriptengine) << "loadEntityScript.contentAvailable" << status << QUrl(url).fileName() << entityID.toString();
1645#endif
1646 if (!isStopping() && _entityScripts.contains(entityID)) {
1647 entityScriptContentAvailable(entityID, url, contents, isURL, success, status);
1648 } else {
1649#ifdef DEBUG_ENTITY_STATES
1650 qCDebug(scriptengine) << "loadEntityScript.contentAvailable -- aborting";
1651#endif
1652 }
1653 // recheck whether us since may have been set to BAD_SCRIPT_UUID_PLACEHOLDER in entityScriptContentAvailable
1654 if (_occupiedScriptURLs.contains(entityScript) && _occupiedScriptURLs[entityScript] == entityID) {
1655 _occupiedScriptURLs.remove(entityScript);
1656 }
1657 });
1658 }, forceRedownload);
1529}1659}
1530 1660
1531// since all of these operations can be asynch we will always do the actual work in the response handler1661// since all of these operations can be asynch we will always do the actual work in the response handler
@@ -1555,25 +1685,51 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co · 23 unmodified lines
1555 1685
1556 auto scriptCache = DependencyManager::get<ScriptCache>();1686 auto scriptCache = DependencyManager::get<ScriptCache>();
1557 bool isFileUrl = isURL && scriptOrURL.startsWith("file://");1687 bool isFileUrl = isURL && scriptOrURL.startsWith("file://");
1558 auto fileName = isURL ? scriptOrURL : "EmbeddedEntityScript";1688 auto fileName = isURL ? scriptOrURL : "about:EmbeddedEntityScript";
1689
1690 const EntityScriptDetails &oldDetails = _entityScripts[entityID];
1691 const QString entityScript = oldDetails.scriptText;
1559 1692
1560 EntityScriptDetails newDetails;1693 EntityScriptDetails newDetails;
1561 newDetails.scriptText = scriptOrURL;1694 newDetails.scriptText = scriptOrURL;
1562 1695
1563 if (!success) {1696 // If an error happens below, we want to update newDetails with the new status info
1564 newDetails.status = EntityScriptStatus::ERROR_LOADING_SCRIPT;1697 // and also abort any pending Entity loads that are waiting on the exact same script URL.
1565 newDetails.errorInfo = "Failed to load script (" + status + ")";1698 auto setError = [&](const QString &errorInfo, const EntityScriptStatus& status) {
1699 newDetails.errorInfo = errorInfo;
1700 newDetails.status = status;
1566 setEntityScriptDetails(entityID, newDetails);1701 setEntityScriptDetails(entityID, newDetails);
1702
1703#ifdef DEBUG_ENTITY_STATES
1704 qCDebug(scriptengine) << "entityScriptContentAvailable -- flagging " << entityScript << " as BAD_SCRIPT_UUID_PLACEHOLDER";
1705#endif
1706 // flag the original entityScript as unusuable
1707 _occupiedScriptURLs[entityScript] = BAD_SCRIPT_UUID_PLACEHOLDER;
1708 processDeferredEntityLoads(entityScript, entityID);
1709 };
1710
1711 // NETWORK / FILESYSTEM ERRORS
1712 if (!success) {
1713 setError("Failed to load script (" + status + ")", EntityScriptStatus::ERROR_LOADING_SCRIPT);
1567 return;1714 return;
1568 }1715 }
1569 1716
1717 // SYNTAX ERRORS
1570 auto syntaxError = lintScript(contents, fileName);1718 auto syntaxError = lintScript(contents, fileName);
1719 if (syntaxError.isError()) {
1720 auto message = syntaxError.property("formatted").toString();
1721 if (message.isEmpty()) {
1722 message = syntaxError.toString();
1723 }
1724 setError(QString("Bad syntax (%1)").arg(message), EntityScriptStatus::ERROR_RUNNING_SCRIPT);
1725 syntaxError.setProperty("detail", entityID.toString());
1726 emit unhandledException(syntaxError);
1727 return;
1728 }
1571 QScriptProgram program { contents, fileName };1729 QScriptProgram program { contents, fileName };
1572 if (!syntaxError.isNull() || program.isNull()) {1730 if (program.isNull()) {
1573 newDetails.status = EntityScriptStatus::ERROR_RUNNING_SCRIPT;1731 setError("Bad program (isNull)", EntityScriptStatus::ERROR_RUNNING_SCRIPT);
1574 newDetails.errorInfo = QString("Bad syntax (%1)").arg(syntaxError);1732 emit unhandledException(makeError("program.isNull"));
1575 setEntityScriptDetails(entityID, newDetails);
1576 qCDebug(scriptengine) << newDetails.errorInfo << scriptOrURL;
1577 return; // done processing script1733 return; // done processing script
1578 }1734 }
1579 1735
@@ -1581,10 +1737,11 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co · 1 unmodified line
1581 setParentURL(scriptOrURL);1737 setParentURL(scriptOrURL);
1582 }1738 }
1583 1739
1740 // SANITY/PERFORMANCE CHECK USING SANDBOX
1584 const int SANDBOX_TIMEOUT = 0.25 * MSECS_PER_SECOND;1741 const int SANDBOX_TIMEOUT = 0.25 * MSECS_PER_SECOND;
1585 BaseScriptEngine sandbox;1742 BaseScriptEngine sandbox;
1586 sandbox.setProcessEventsInterval(SANDBOX_TIMEOUT);1743 sandbox.setProcessEventsInterval(SANDBOX_TIMEOUT);
1587 QScriptValue testConstructor;1744 QScriptValue testConstructor, exception;
1588 {1745 {
1589 QTimer timeout;1746 QTimer timeout;
1590 timeout.setSingleShot(true);1747 timeout.setSingleShot(true);
@@ -1598,18 +1755,26 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co · 7 unmodified lines
1598 context->throwError(QString("Timed out (entity constructors are limited to %1ms)").arg(SANDBOX_TIMEOUT));1755 context->throwError(QString("Timed out (entity constructors are limited to %1ms)").arg(SANDBOX_TIMEOUT));
1599 }1756 }
1600 });1757 });
1758
1601 testConstructor = sandbox.evaluate(program);1759 testConstructor = sandbox.evaluate(program);
1760
1761 if (sandbox.hasUncaughtException()) {
1762 exception = sandbox.cloneUncaughtException(QString("(preflight %1)").arg(entityID.toString()));
1763 sandbox.clearExceptions();
1764 } else if (testConstructor.isError()) {
1765 exception = testConstructor;
1766 }
1602 }1767 }
1603 1768
1604 QString exceptionMessage = sandbox.formatUncaughtException(program.fileName());1769 if (exception.isError()) {
1605 if (!exceptionMessage.isNull()) {1770 // create a local copy using makeError to decouple from the sandbox engine
1606 newDetails.status = EntityScriptStatus::ERROR_RUNNING_SCRIPT;1771 exception = makeError(exception);
1607 newDetails.errorInfo = exceptionMessage;1772 setError(formatException(exception), EntityScriptStatus::ERROR_RUNNING_SCRIPT);
1608 setEntityScriptDetails(entityID, newDetails);1773 emit unhandledException(exception);
1609 qCDebug(scriptengine) << "----- ScriptEngine::entityScriptContentAvailable -- hadUncaughtExceptions (" << scriptOrURL << ")";
1610 return;1774 return;
1611 }1775 }
1612 1776
1777 // CONSTRUCTOR VIABILITY
1613 if (!testConstructor.isFunction()) {1778 if (!testConstructor.isFunction()) {
1614 QString testConstructorType = QString(testConstructor.toVariant().typeName());1779 QString testConstructorType = QString(testConstructor.toVariant().typeName());
1615 if (testConstructorType == "") {1780 if (testConstructorType == "") {
@@ -1620,32 +1785,48 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co · 4 unmodified lines
1620 if (testConstructorValue.size() > maxTestConstructorValueSize) {1785 if (testConstructorValue.size() > maxTestConstructorValueSize) {
1621 testConstructorValue = testConstructorValue.mid(0, maxTestConstructorValueSize) + "...";1786 testConstructorValue = testConstructorValue.mid(0, maxTestConstructorValueSize) + "...";
1622 }1787 }
1623 scriptErrorMessage("Error -- ScriptEngine::loadEntityScript() entity:" + entityID.toString()1788 auto message = QString("failed to load entity script -- expected a function, got %1, %2")
1624 + "failed to load entity script -- expected a function, got " + testConstructorType1789 .arg(testConstructorType).arg(testConstructorValue);
1625 + "," + testConstructorValue
1626 + "," + scriptOrURL);
1627 1790
1628 newDetails.status = EntityScriptStatus::ERROR_RUNNING_SCRIPT;1791 auto err = makeError(message);
1629 newDetails.errorInfo = "Could not find constructor";1792 err.setProperty("fileName", scriptOrURL);
1630 setEntityScriptDetails(entityID, newDetails);1793 err.setProperty("detail", "(constructor " + entityID.toString() + ")");
1631 1794
1632 qCDebug(scriptengine) << "----- ScriptEngine::entityScriptContentAvailable -- failed to run (" << scriptOrURL << ")";1795 setError("Could not find constructor (" + testConstructorType + ")", EntityScriptStatus::ERROR_RUNNING_SCRIPT);
1796 emit unhandledException(err);
1633 return; // done processing script1797 return; // done processing script
1634 }1798 }
1635 1799
1800 // (this feeds into refreshFileScript)
1636 int64_t lastModified = 0;1801 int64_t lastModified = 0;
1637 if (isFileUrl) {1802 if (isFileUrl) {
1638 QString file = QUrl(scriptOrURL).toLocalFile();1803 QString file = QUrl(scriptOrURL).toLocalFile();
1639 lastModified = (quint64)QFileInfo(file).lastModified().toMSecsSinceEpoch();1804 lastModified = (quint64)QFileInfo(file).lastModified().toMSecsSinceEpoch();
1640 }1805 }
1806
1807 // THE ACTUAL EVALUATION AND CONSTRUCTION
1641 QScriptValue entityScriptConstructor, entityScriptObject;1808 QScriptValue entityScriptConstructor, entityScriptObject;
1642 QUrl sandboxURL = currentSandboxURL.isEmpty() ? scriptOrURL : currentSandboxURL;1809 QUrl sandboxURL = currentSandboxURL.isEmpty() ? scriptOrURL : currentSandboxURL;
1643 auto initialization = [&]{1810 auto initialization = [&]{
1644 entityScriptConstructor = evaluate(contents, fileName);1811 entityScriptConstructor = evaluate(contents, fileName);
1645 entityScriptObject = entityScriptConstructor.construct();1812 entityScriptObject = entityScriptConstructor.construct();
1813
1814 if (hasUncaughtException()) {
1815 entityScriptObject = cloneUncaughtException("(construct " + entityID.toString() + ")");
1816 clearExceptions();
1817 }
1646 };1818 };
1819
1647 doWithEnvironment(entityID, sandboxURL, initialization);1820 doWithEnvironment(entityID, sandboxURL, initialization);
1648 1821
1822 if (entityScriptObject.isError()) {
1823 auto exception = entityScriptObject;
1824 setError(formatException(exception), EntityScriptStatus::ERROR_RUNNING_SCRIPT);
1825 emit unhandledException(exception);
1826 return;
1827 }
1828
1829 // ... AND WE HAVE LIFTOFF
1649 newDetails.status = EntityScriptStatus::RUNNING;1830 newDetails.status = EntityScriptStatus::RUNNING;
1650 newDetails.scriptObject = entityScriptObject;1831 newDetails.scriptObject = entityScriptObject;
1651 newDetails.lastModified = lastModified;1832 newDetails.lastModified = lastModified;
@@ -1658,6 +1839,9 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co · 6 unmodified lines
1658 1839
1659 // if we got this far, then call the preload method1840 // if we got this far, then call the preload method
1660 callEntityScriptMethod(entityID, "preload");1841 callEntityScriptMethod(entityID, "preload");
1842
1843 _occupiedScriptURLs.remove(entityScript);
1844 processDeferredEntityLoads(entityScript, entityID);
1661}1845}
1662 1846
1663void ScriptEngine::unloadEntityScript(const EntityItemID& entityID) {1847void ScriptEngine::unloadEntityScript(const EntityItemID& entityID) {
@@ -1677,13 +1861,25 @@ void ScriptEngine::unloadEntityScript(const EntityItemID& entityID) { · 13 unmodified lines
1677#endif1861#endif
1678 1862
1679 if (_entityScripts.contains(entityID)) {1863 if (_entityScripts.contains(entityID)) {
1864 const EntityScriptDetails &oldDetails = _entityScripts[entityID];
1680 if (isEntityScriptRunning(entityID)) {1865 if (isEntityScriptRunning(entityID)) {
1681 callEntityScriptMethod(entityID, "unload");1866 callEntityScriptMethod(entityID, "unload");
1867 } else {
1868 qCDebug(scriptengine) << "unload called while !running" << entityID << oldDetails.status;
1869 }
1870 if (oldDetails.status != EntityScriptStatus::UNLOADED) {
1871 EntityScriptDetails newDetails;
1872 newDetails.status = EntityScriptStatus::UNLOADED;
1873 newDetails.lastModified = QDateTime::currentMSecsSinceEpoch();
1874 // keep scriptText populated for the current need to "debouce" duplicate calls to unloadEntityScript
1875 newDetails.scriptText = oldDetails.scriptText;
1876 setEntityScriptDetails(entityID, newDetails);
1682 }1877 }
1683 EntityScriptDetails newDetails;
1684 newDetails.status = EntityScriptStatus::UNLOADED;
1685 setEntityScriptDetails(entityID, newDetails);
1686 stopAllTimersForEntityScript(entityID);1878 stopAllTimersForEntityScript(entityID);
1879 {
1880 // FIXME: shouldn't have to do this here, but currently something seems to be firing unloads moments after firing initial load requests
1881 processDeferredEntityLoads(oldDetails.scriptText, entityID);
1882 }
1687 }1883 }
1688}1884}
1689 1885
@@ -1704,15 +1900,14 @@ void ScriptEngine::unloadAllEntityScripts() { · 14 unmodified lines
1704 }1900 }
1705 _entityScripts.clear();1901 _entityScripts.clear();
1706 emit entityScriptDetailsUpdated();1902 emit entityScriptDetailsUpdated();
1903 _occupiedScriptURLs.clear();
1707 1904
1708#ifdef DEBUG_ENGINE_STATE1905#ifdef DEBUG_ENGINE_STATE
1709 qCDebug(scriptengine) << "---- CURRENT STATE OF ENGINE: --------------------------";1906 _debugDump(
1710 QScriptValueIterator it(globalObject());1907 "---- CURRENT STATE OF ENGINE: --------------------------",
1711 while (it.hasNext()) {1908 globalObject(),
1712 it.next();1909 "--------------------------------------------------------"
1713 qCDebug(scriptengine) << it.name() << ":" << it.value().toString();1910 );
1714 }
1715 qCDebug(scriptengine) << "--------------------------------------------------------";
1716#endif // DEBUG_ENGINE_STATE1911#endif // DEBUG_ENGINE_STATE
1717}1912}
1718 1913
@@ -1734,17 +1929,7 @@ void ScriptEngine::refreshFileScript(const EntityItemID& entityID) { · 15 unmodified lines
1734 auto lastModified = QFileInfo(filePath).lastModified().toMSecsSinceEpoch();1929 auto lastModified = QFileInfo(filePath).lastModified().toMSecsSinceEpoch();
1735 if (lastModified > details.lastModified) {1930 if (lastModified > details.lastModified) {
1736 scriptInfoMessage("Reloading modified script " + details.scriptText);1931 scriptInfoMessage("Reloading modified script " + details.scriptText);
1737 1932 loadEntityScript(entityID, details.scriptText, true);
1738 QFile file(filePath);
1739 file.open(QIODevice::ReadOnly);
1740 QString scriptContents = QTextStream(&file).readAll();
1741 this->unloadEntityScript(entityID);
1742 this->entityScriptContentAvailable(entityID, details.scriptText, scriptContents, true, true, "Success");
1743 if (!isEntityScriptRunning(entityID)) {
1744 scriptWarningMessage("Reload script " + details.scriptText + " failed");
1745 } else {
1746 details = _entityScripts[entityID];
1747 }
1748 }1933 }
1749 }1934 }
1750 recurseGuard = false;1935 recurseGuard = false;
@@ -1768,14 +1953,14 @@ void ScriptEngine::doWithEnvironment(const EntityItemID& entityID, const QUrl& s · 17 unmodified lines
1768#else1953#else
1769 operation();1954 operation();
1770#endif1955#endif
1771 if (hasUncaughtException()) {1956 if (!isEvaluating() && hasUncaughtException()) {
1772 reportUncaughtException();1957 emit unhandledException(cloneUncaughtException(!entityID.isNull() ? entityID.toString() : __FUNCTION__));
1773 clearExceptions();1958 clearExceptions();
1774 }1959 }
1775
1776 currentEntityIdentifier = oldIdentifier;1960 currentEntityIdentifier = oldIdentifier;
1777 currentSandboxURL = oldSandboxURL;1961 currentSandboxURL = oldSandboxURL;
1778}1962}
1963
1779void ScriptEngine::callWithEnvironment(const EntityItemID& entityID, const QUrl& sandboxURL, QScriptValue function, QScriptValue thisObject, QScriptValueList args) {1964void ScriptEngine::callWithEnvironment(const EntityItemID& entityID, const QUrl& sandboxURL, QScriptValue function, QScriptValue thisObject, QScriptValueList args) {
1780 auto operation = [&]() {1965 auto operation = [&]() {
1781 function.call(thisObject, args);1966 function.call(thisObject, args);
@@ -1850,7 +2035,6 @@ void ScriptEngine::callEntityScriptMethod(const EntityItemID& entityID, const QS · 68 unmodified lines
1850 }2035 }
1851}2036}
1852 2037
1853
1854void ScriptEngine::callEntityScriptMethod(const EntityItemID& entityID, const QString& methodName, const EntityItemID& otherID, const Collision& collision) {2038void ScriptEngine::callEntityScriptMethod(const EntityItemID& entityID, const QString& methodName, const EntityItemID& otherID, const Collision& collision) {
1855 if (QThread::currentThread() != thread()) {2039 if (QThread::currentThread() != thread()) {
1856#ifdef THREAD_DEBUGGING2040#ifdef THREAD_DEBUGGING
@@ -1885,3 +2069,4 @@ void ScriptEngine::callEntityScriptMethod(const EntityItemID& entityID, const QS · 28 unmodified lines
1885 }2069 }
1886 }2070 }
1887}2071}
2072