History

separate out supporting changes into 21114-part2 branch

humbletim committed 9 years ago

Commit b1595621 parent 9b0bee910 files · +833 −223Browse files

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

Unified · Split

libraries/script-engine/src/ScriptEngine.cpp+342−191
@@ -49,6 +49,7 @@ · 48 unmodified lines
49 49
50#include "ArrayBufferViewClass.h"50#include "ArrayBufferViewClass.h"
51#include "BatchLoader.h"51#include "BatchLoader.h"
52#include "BaseScriptEngine.h"
52#include "DataViewClass.h"53#include "DataViewClass.h"
53#include "EventTypes.h"54#include "EventTypes.h"
54#include "FileScriptingInterface.h" // unzip project55#include "FileScriptingInterface.h" // unzip project
@@ -68,9 +69,12 @@ · 13 unmodified lines
68 69
69#include "MIDIEvent.h"70#include "MIDIEvent.h"
70 71
71const QString BaseScriptEngine::SCRIPT_EXCEPTION_FORMAT { "[UncaughtException] %1 in %2:%3" };
72static const QScriptEngine::QObjectWrapOptions DEFAULT_QOBJECT_WRAP_OPTIONS =72static const QScriptEngine::QObjectWrapOptions DEFAULT_QOBJECT_WRAP_OPTIONS =
73 QScriptEngine::ExcludeDeleteLater | QScriptEngine::ExcludeChildObjects;73 QScriptEngine::ExcludeDeleteLater | QScriptEngine::ExcludeChildObjects;
74static const QScriptValue::PropertyFlags READONLY_PROP_FLAGS { QScriptValue::ReadOnly | QScriptValue::Undeletable };
75static const QScriptValue::PropertyFlags HIDDEN_PROP_FLAGS { READONLY_PROP_FLAGS | QScriptValue::SkipInEnumeration };
76
77
74 78
75static const bool HIFI_AUTOREFRESH_FILE_SCRIPTS { true };79static const bool HIFI_AUTOREFRESH_FILE_SCRIPTS { true };
76 80
@@ -89,13 +93,9 @@ static QScriptValue debugPrint(QScriptContext* context, QScriptEngine* engine){ · 12 unmodified lines
89 }93 }
90 qCDebug(scriptengineScript).noquote() << "script:print()<<" << message; // noquote() so that \n is treated as newline94 qCDebug(scriptengineScript).noquote() << "script:print()<<" << message; // noquote() so that \n is treated as newline
91 95
92 message = message.replace("\\", "\\\\")96 // FIXME - this approach neeeds revisiting. print() comes here, which ends up calling Script.print?
93 .replace("\n", "\\n")97 engine->globalObject().property("Script").property("print")
94 .replace("\r", "\\r")98 .call(engine->nullValue(), QScriptValueList({ message }));
95 .replace("'", "\\'");
96
97 // FIXME - this approach neeeds revisiting. print() comes here, which ends up doing an evaluate?
98 engine->evaluate("Script.print('" + message + "')");
99 99
100 return QScriptValue();100 return QScriptValue();
101}101}
@@ -139,52 +139,15 @@ QString encodeEntityIdIntoEntityUrl(const QString& url, const QString& entityID) · 37 unmodified lines
139 return url + " [EntityID:" + entityID + "]";139 return url + " [EntityID:" + entityID + "]";
140}140}
141 141
142QString BaseScriptEngine::lintScript(const QString& sourceCode, const QString& fileName, const int lineNumber) {142QString ScriptEngine::logException(const QScriptValue& exception) {
143 const auto syntaxCheck = checkSyntax(sourceCode);143 auto message = formatException(exception);
144 if (syntaxCheck.state() != syntaxCheck.Valid) {
145 const auto error = syntaxCheck.errorMessage();
146 const auto line = QString::number(syntaxCheck.errorLineNumber());
147 const auto column = QString::number(syntaxCheck.errorColumnNumber());
148 const auto message = QString("[SyntaxError] %1 in %2:%3(%4)").arg(error, fileName, line, column);
149 return message;
150 }
151 return QString();
152}
153
154QString BaseScriptEngine::formatUncaughtException(const QString& overrideFileName) {
155 QString message;
156 if (hasUncaughtException()) {
157 const auto error = uncaughtException();
158 const auto backtrace = uncaughtExceptionBacktrace();
159 const auto exception = error.toString();
160 auto filename = overrideFileName;
161 if (filename.isEmpty()) {
162 QScriptContextInfo ctx { currentContext() };
163 filename = ctx.fileName();
164 }
165 const auto line = QString::number(uncaughtExceptionLineNumber());
166
167 message = QString(SCRIPT_EXCEPTION_FORMAT).arg(exception, overrideFileName, line);
168 if (!backtrace.empty()) {
169 static const auto lineSeparator = "\n ";
170 message += QString("\n[Backtrace]%1%2").arg(lineSeparator, backtrace.join(lineSeparator));
171 }
172 }
173 return message;
174}
175
176QString ScriptEngine::reportUncaughtException(const QString& overrideFileName) {
177 QString message;
178 if (!hasUncaughtException()) {
179 return message;
180 }
181 message = formatUncaughtException(overrideFileName.isEmpty() ? _fileNameString : overrideFileName);
182 scriptErrorMessage(qPrintable(message));144 scriptErrorMessage(qPrintable(message));
183 return message;145 return message;
184}146}
185 147
186int ScriptEngine::processLevelMaxRetries { ScriptRequest::MAX_RETRIES };148int ScriptEngine::processLevelMaxRetries { ScriptRequest::MAX_RETRIES };
187ScriptEngine::ScriptEngine(Context context, const QString& scriptContents, const QString& fileNameString) :149ScriptEngine::ScriptEngine(Context context, const QString& scriptContents, const QString& fileNameString) :
150 BaseScriptEngine(),
188 _context(context),151 _context(context),
189 _scriptContents(scriptContents),152 _scriptContents(scriptContents),
190 _timerFunctionMap(),153 _timerFunctionMap(),
@@ -194,8 +157,14 @@ ScriptEngine::ScriptEngine(Context context, const QString& scriptContents, const · 3 unmodified lines
194 DependencyManager::get<ScriptEngines>()->addScriptEngine(this);157 DependencyManager::get<ScriptEngines>()->addScriptEngine(this);
195 158
196 connect(this, &QScriptEngine::signalHandlerException, this, [this](const QScriptValue& exception) {159 connect(this, &QScriptEngine::signalHandlerException, this, [this](const QScriptValue& exception) {
197 reportUncaughtException();160 if (hasUncaughtException()) {
198 clearExceptions();161 // the engine's uncaughtException() seems to produce much better stack traces here
162 emit unhandledException(cloneUncaughtException("signalHandlerException"));
163 clearExceptions();
164 } else {
165 // ... but may not always be available -- so if needed we fallback to the passed exception
166 emit unhandledException(exception);
167 }
199 });168 });
200 169
201 setProcessEventsInterval(MSECS_PER_SECOND);170 setProcessEventsInterval(MSECS_PER_SECOND);
@@ -203,7 +172,16 @@ ScriptEngine::ScriptEngine(Context context, const QString& scriptContents, const · 1 unmodified line
203 qCDebug(scriptengine) << "isEntityServerScript() -- limiting maxRetries to 1";172 qCDebug(scriptengine) << "isEntityServerScript() -- limiting maxRetries to 1";
204 processLevelMaxRetries = 1;173 processLevelMaxRetries = 1;
205 }174 }
206 qCDebug(scriptengine) << getContext() << "processLevelMaxRetries =" << processLevelMaxRetries;175 //qCDebug(scriptengine) << getContext() << "processLevelMaxRetries =" << processLevelMaxRetries << fileNameString;
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 });
207}185}
208 186
209QString ScriptEngine::getContext() const {187QString ScriptEngine::getContext() const {
@@ -223,13 +201,22 @@ QString ScriptEngine::getContext() const { · 13 unmodified lines
223}201}
224 202
225ScriptEngine::~ScriptEngine() {203ScriptEngine::~ScriptEngine() {
204 // FIXME: are these scriptInfoMessage/scriptWarningMessage segfaulting anybody else at app shutdown?
205#if !defined(Q_OS_LINUX)
226 scriptInfoMessage("Script Engine shutting down:" + getFilename());206 scriptInfoMessage("Script Engine shutting down:" + getFilename());
207#else
208 qCDebug(scriptengine) << "~ScriptEngine()" << this;
209#endif
227 210
228 auto scriptEngines = DependencyManager::get<ScriptEngines>();211 auto scriptEngines = DependencyManager::get<ScriptEngines>();
229 if (scriptEngines) {212 if (scriptEngines) {
230 scriptEngines->removeScriptEngine(this);213 scriptEngines->removeScriptEngine(this);
231 } else {214 } else {
215#if !defined(Q_OS_LINUX)
232 scriptWarningMessage("Script destroyed after ScriptEngines!");216 scriptWarningMessage("Script destroyed after ScriptEngines!");
217#else
218 qCWarning(scriptengine) << ("Script destroyed after ScriptEngines!");
219#endif
233 }220 }
234}221}
235 222
@@ -319,9 +306,12 @@ void ScriptEngine::runDebuggable() { · 83 unmodified lines
319 }306 }
320 }307 }
321 _lastUpdate = now;308 _lastUpdate = now;
322 // Debug and clear exceptions309
323 if (hasUncaughtException()) {310 // only clear exceptions if we are not in the middle of evaluating
324 reportUncaughtException();311 if (!isEvaluating() && hasUncaughtException()) {
312 qCWarning(scriptengine) << __FUNCTION__ << "---------- UNCAUGHT EXCEPTION --------";
313 qCWarning(scriptengine) << "runDebuggable" << uncaughtException().toString();
314 logException(__FUNCTION__);
325 clearExceptions();315 clearExceptions();
326 }316 }
327 });317 });
@@ -356,10 +346,9 @@ void ScriptEngine::runInThread() { · 28 unmodified lines
356 workerThread->start();346 workerThread->start();
357}347}
358 348
359void ScriptEngine::executeOnScriptThread(std::function<void()> function, bool blocking ) {349void ScriptEngine::executeOnScriptThread(std::function<void()> function, const Qt::ConnectionType& type ) {
360 if (QThread::currentThread() != thread()) {350 if (QThread::currentThread() != thread()) {
361 QMetaObject::invokeMethod(this, "executeOnScriptThread", blocking ? Qt::BlockingQueuedConnection : Qt::QueuedConnection,351 QMetaObject::invokeMethod(this, "executeOnScriptThread", type, Q_ARG(std::function<void()>, function));
362 Q_ARG(std::function<void()>, function));
363 return;352 return;
364 }353 }
365 354
@@ -558,6 +547,7 @@ void ScriptEngine::init() { · 192 unmodified lines
558 qCWarning(scriptengine) << "deletingEntity while entity script is still running!" << entityID;547 qCWarning(scriptengine) << "deletingEntity while entity script is still running!" << entityID;
559 }548 }
560 _entityScripts.remove(entityID);549 _entityScripts.remove(entityID);
550 emit entityScriptDetailsUpdated();
561 }551 }
562 });552 });
563 553
@@ -589,8 +579,7 @@ void ScriptEngine::init() { · 25 unmodified lines
589 QScriptValue webSocketConstructorValue = newFunction(WebSocketClass::constructor);579 QScriptValue webSocketConstructorValue = newFunction(WebSocketClass::constructor);
590 globalObject().setProperty("WebSocket", webSocketConstructorValue);580 globalObject().setProperty("WebSocket", webSocketConstructorValue);
591 581
592 QScriptValue printConstructorValue = newFunction(debugPrint);582 globalObject().setProperty("print", newFunction(debugPrint));
593 globalObject().setProperty("print", printConstructorValue);
594 583
595 QScriptValue audioEffectOptionsConstructorValue = newFunction(AudioEffectOptions::constructor);584 QScriptValue audioEffectOptionsConstructorValue = newFunction(AudioEffectOptions::constructor);
596 globalObject().setProperty("AudioEffectOptions", audioEffectOptionsConstructorValue);585 globalObject().setProperty("AudioEffectOptions", audioEffectOptionsConstructorValue);
@@ -604,6 +593,7 @@ void ScriptEngine::init() { · 7 unmodified lines
604 qScriptRegisterMetaType(this, wscReadyStateToScriptValue, wscReadyStateFromScriptValue);593 qScriptRegisterMetaType(this, wscReadyStateToScriptValue, wscReadyStateFromScriptValue);
605 594
606 registerGlobalObject("Script", this);595 registerGlobalObject("Script", this);
596
607 registerGlobalObject("Audio", &AudioScriptingInterface::getInstance());597 registerGlobalObject("Audio", &AudioScriptingInterface::getInstance());
608 registerGlobalObject("Entities", entityScriptingInterface.data());598 registerGlobalObject("Entities", entityScriptingInterface.data());
609 registerGlobalObject("Quat", &_quatLibrary);599 registerGlobalObject("Quat", &_quatLibrary);
@@ -870,7 +860,6 @@ void ScriptEngine::addEventHandler(const EntityItemID& entityID, const QString& · 260 unmodified lines
870 handlersForEvent << handlerData; // Note that the same handler can be added many times. See removeEntityEventHandler().860 handlersForEvent << handlerData; // Note that the same handler can be added many times. See removeEntityEventHandler().
871}861}
872 862
873
874QScriptValue ScriptEngine::evaluate(const QString& sourceCode, const QString& fileName, int lineNumber) {863QScriptValue ScriptEngine::evaluate(const QString& sourceCode, const QString& fileName, int lineNumber) {
875 if (DependencyManager::get<ScriptEngines>()->isStopped()) {864 if (DependencyManager::get<ScriptEngines>()->isStopped()) {
876 return QScriptValue(); // bail early865 return QScriptValue(); // bail early
@@ -892,23 +881,28 @@ QScriptValue ScriptEngine::evaluate(const QString& sourceCode, const QString& fi · 15 unmodified lines
892 881
893 // Check syntax882 // Check syntax
894 auto syntaxError = lintScript(sourceCode, fileName);883 auto syntaxError = lintScript(sourceCode, fileName);
884 if (syntaxError.isError()) {
885 if (isEvaluating()) {
886 currentContext()->throwValue(syntaxError);
887 } else {
888 syntaxError.setProperty("detail", "evaluate");
889 emit unhandledException(syntaxError);
890 }
891 return syntaxError;
892 }
895 QScriptProgram program { sourceCode, fileName, lineNumber };893 QScriptProgram program { sourceCode, fileName, lineNumber };
896 if (!syntaxError.isEmpty() || program.isNull()) {894 if (program.isNull()) {
897 scriptErrorMessage(qPrintable(syntaxError));895 // can this happen?
898 return QScriptValue();896 auto err = makeError("could not create QScriptProgram for " + fileName);
897 emit unhandledException(err);
898 return err;
899 }899 }
900 900
901 ++_evaluatesPending;901 QScriptValue result;
902 auto result = BaseScriptEngine::evaluate(program);902 {
903 --_evaluatesPending;903 // catch any exceptions throw by nested operations
904 904 ExceptionEmitter tryCatch(this, __FUNCTION__);
905 if (hasUncaughtException()) {905 result = BaseScriptEngine::evaluate(program);
906 result = uncaughtException();
907 reportUncaughtException(program.fileName());
908 emit evaluationFinished(result, true);
909 clearExceptions();
910 } else {
911 emit evaluationFinished(result, false);
912 }906 }
913 return result;907 return result;
914}908}
@@ -929,8 +923,11 @@ void ScriptEngine::run() { · 14 unmodified lines
929 _isRunning = true;923 _isRunning = true;
930 emit runningStateChanged();924 emit runningStateChanged();
931 925
932 QScriptValue result = evaluate(_scriptContents, _fileNameString);926 {
933 927 // catch any exceptions throw by nested operations
928 ExceptionEmitter tryCatch(this, __FUNCTION__);
929 evaluate(_scriptContents, _fileNameString);
930 }
934#ifdef _WIN32931#ifdef _WIN32
935 // VS13 does not sleep_until unless it uses the system_clock, see:932 // VS13 does not sleep_until unless it uses the system_clock, see:
936 // https://www.reddit.com/r/cpp_questions/comments/3o71ic/sleep_until_not_working_with_a_time_pointsteady/933 // https://www.reddit.com/r/cpp_questions/comments/3o71ic/sleep_until_not_working_with_a_time_pointsteady/
@@ -1057,13 +1054,14 @@ void ScriptEngine::run() { · 120 unmodified lines
1057 }1054 }
1058 _lastUpdate = now;1055 _lastUpdate = now;
1059 1056
1060 // Debug and clear exceptions1057 // only clear exceptions if we are not in the middle of evaluating
1061 if (hasUncaughtException()) {1058 if (!isEvaluating() && hasUncaughtException()) {
1062 reportUncaughtException();1059 qCWarning(scriptengine) << __FUNCTION__ << "---------- UNCAUGHT EXCEPTION --------";
1060 qCWarning(scriptengine) << "runInThread" << uncaughtException().toString();
1061 emit unhandledException(cloneUncaughtException(__FUNCTION__));
1063 clearExceptions();1062 clearExceptions();
1064 }1063 }
1065 }1064 }
1066
1067 scriptInfoMessage("Script Engine stopping:" + getFilename());1065 scriptInfoMessage("Script Engine stopping:" + getFilename());
1068 1066
1069 stopAllTimers(); // make sure all our timers are stopped if the script is ending1067 stopAllTimers(); // make sure all our timers are stopped if the script is ending
@@ -1096,9 +1094,11 @@ void ScriptEngine::run() { · 26 unmodified lines
1096// we want to only call it in our own run "shutdown" processing.1094// we want to only call it in our own run "shutdown" processing.
1097void ScriptEngine::stopAllTimers() {1095void ScriptEngine::stopAllTimers() {
1098 QMutableHashIterator<QTimer*, CallbackData> i(_timerFunctionMap);1096 QMutableHashIterator<QTimer*, CallbackData> i(_timerFunctionMap);
1097 int j {0};
1099 while (i.hasNext()) {1098 while (i.hasNext()) {
1100 i.next();1099 i.next();
1101 QTimer* timer = i.key();1100 QTimer* timer = i.key();
1101 qCDebug(scriptengine) << getFilename() << "stopAllTimers[" << j++ << "]";
1102 stopTimer(timer);1102 stopTimer(timer);
1103 }1103 }
1104}1104}
@@ -1185,6 +1185,7 @@ void ScriptEngine::timerFired() { · 80 unmodified lines
1185 _timerFunctionMap.remove(callingTimer);1185 _timerFunctionMap.remove(callingTimer);
1186 delete callingTimer;1186 delete callingTimer;
1187 }1187 }
1188 //qCWarning(scriptengine) << "timerFired" << timerData.function.toString();
1188 1189
1189 // call the associated JS function, if it exists1190 // call the associated JS function, if it exists
1190 if (timerData.function.isValid()) {1191 if (timerData.function.isValid()) {
@@ -1193,11 +1194,11 @@ void ScriptEngine::timerFired() { · 2 unmodified lines
1193 auto postTimer = p_high_resolution_clock::now();1194 auto postTimer = p_high_resolution_clock::now();
1194 auto elapsed = (postTimer - preTimer);1195 auto elapsed = (postTimer - preTimer);
1195 _totalTimerExecution += std::chrono::duration_cast<std::chrono::microseconds>(elapsed);1196 _totalTimerExecution += std::chrono::duration_cast<std::chrono::microseconds>(elapsed);
1196 1197 } else {
1198 qCWarning(scriptengine) << "timerFired -- invalid function" << timerData.function.toVariant().toString();
1197 }1199 }
1198}1200}
1199 1201
1200
1201QObject* ScriptEngine::setupTimerWithInterval(const QScriptValue& function, int intervalMS, bool isSingleShot) {1202QObject* ScriptEngine::setupTimerWithInterval(const QScriptValue& function, int intervalMS, bool isSingleShot) {
1202 // create the timer, add it to the map, and start it1203 // create the timer, add it to the map, and start it
1203 QTimer* newTimer = new QTimer(this);1204 QTimer* newTimer = new QTimer(this);
@@ -1214,7 +1215,7 @@ QObject* ScriptEngine::setupTimerWithInterval(const QScriptValue& function, int · 10 unmodified lines
1214 // make sure the timer stops when the script does1215 // make sure the timer stops when the script does
1215 connect(this, &ScriptEngine::scriptEnding, newTimer, &QTimer::stop);1216 connect(this, &ScriptEngine::scriptEnding, newTimer, &QTimer::stop);
1216 1217
1217 CallbackData timerData = {function, currentEntityIdentifier, currentSandboxURL };1218 CallbackData timerData = { function, currentEntityIdentifier, currentSandboxURL };
1218 _timerFunctionMap.insert(newTimer, timerData);1219 _timerFunctionMap.insert(newTimer, timerData);
1219 1220
1220 newTimer->start(intervalMS);1221 newTimer->start(intervalMS);
@@ -1244,35 +1245,46 @@ void ScriptEngine::stopTimer(QTimer *timer) { · 23 unmodified lines
1244 timer->stop();1245 timer->stop();
1245 _timerFunctionMap.remove(timer);1246 _timerFunctionMap.remove(timer);
1246 delete timer;1247 delete timer;
1248 } else {
1249 qCDebug(scriptengine) << "stopTimer -- not in _timerFunctionMap" << timer;
1247 }1250 }
1248}1251}
1249 1252
1250QUrl ScriptEngine::resolvePath(const QString& include) const {1253QUrl ScriptEngine::resolvePath(const QString& include) const {
1251 QUrl url(include);1254 QUrl url(include);
1252 // first lets check to see if it's already a full URL1255 // first lets check to see if it's already a full URL
1253 if (!url.scheme().isEmpty()) {1256 if (include.startsWith("/") || url.scheme().length() == 1) {
1257 url = QUrl::fromLocalFile(include);
1258 }
1259 if (!url.isRelative()) {
1254 return expandScriptUrl(url);1260 return expandScriptUrl(url);
1255 }1261 }
1256 1262
1257 QScriptContextInfo contextInfo { currentContext()->parentContext() };
1258
1259
1260 // we apparently weren't a fully qualified url, so, let's assume we're relative1263 // we apparently weren't a fully qualified url, so, let's assume we're relative
1261 // to the original URL of our script1264 // to the first absolute URL in the JS scope chain
1262 QUrl parentURL = contextInfo.fileName();1265 QUrl parentURL;
1263 if (parentURL.isEmpty()) {1266 auto ctx = currentContext();
1264 if (_parentURL.isEmpty()) {1267 do {
1265 parentURL = QUrl(_fileNameString);1268 QScriptContextInfo contextInfo { ctx };
1266 } else {1269 parentURL = QUrl(contextInfo.fileName());
1267 parentURL = QUrl(_parentURL);1270 ctx = ctx->parentContext();
1271 } while (parentURL.isRelative() && ctx);
1272
1273 if (parentURL.isRelative()) {
1274 // fallback to the "include" parent (if defined, this will already be absolute)
1275 parentURL = QUrl(_parentURL);
1276 }
1277
1278 if (parentURL.isRelative()) {
1279 // fallback to the original script engine URL
1280 parentURL = QUrl(_fileNameString);
1281
1282 // if still relative and path-like, then this is probably a local file...
1283 if (parentURL.isRelative() && url.path().contains("/")) {
1284 parentURL = QUrl::fromLocalFile(_fileNameString);
1268 }1285 }
1269 }1286 }
1270 1287
1271 // if the parent URL's scheme is empty, then this is probably a local file...
1272 if (parentURL.scheme().isEmpty()) {
1273 parentURL = QUrl::fromLocalFile(_fileNameString);
1274 }
1275
1276 // at this point we should have a legitimate fully qualified URL for our parent1288 // at this point we should have a legitimate fully qualified URL for our parent
1277 url = expandScriptUrl(parentURL.resolved(url));1289 url = expandScriptUrl(parentURL.resolved(url));
1278 return url;1290 return url;
@@ -1297,22 +1309,7 @@ void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callbac · 18 unmodified lines
1297 return; // bail early1309 return; // bail early
1298 }1310 }
1299 QList<QUrl> urls;1311 QList<QUrl> urls;
1300 bool knowsSensitivity = false;
1301 Qt::CaseSensitivity sensitivity { Qt::CaseSensitive };
1302 auto getSensitivity = [&]() {
1303 if (!knowsSensitivity) {
1304 QString path = currentSandboxURL.path();
1305 QFileInfo upperFI(path.toUpper());
1306 QFileInfo lowerFI(path.toLower());
1307 sensitivity = (upperFI == lowerFI) ? Qt::CaseInsensitive : Qt::CaseSensitive;
1308 knowsSensitivity = true;
1309 }
1310 return sensitivity;
1311 };
1312 1312
1313 // Guard against meaningless query and fragment parts.
1314 // Do NOT use PreferLocalFile as its behavior is unpredictable (e.g., on defaultScriptsLocation())
1315 const auto strippingFlags = QUrl::RemoveFilename | QUrl::RemoveQuery | QUrl::RemoveFragment;
1316 for (QString includeFile : includeFiles) {1313 for (QString includeFile : includeFiles) {
1317 QString file = ResourceManager::normalizeURL(includeFile);1314 QString file = ResourceManager::normalizeURL(includeFile);
1318 QUrl thisURL;1315 QUrl thisURL;
@@ -1329,10 +1326,8 @@ void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callbac · 10 unmodified lines
1329 thisURL = resolvePath(file);1326 thisURL = resolvePath(file);
1330 }1327 }
1331 1328
1332 if (!isStandardLibrary && !currentSandboxURL.isEmpty() && (thisURL.scheme() == "file") &&1329 bool disallowOutsideFiles = thisURL.isLocalFile() && !isStandardLibrary && !currentSandboxURL.isLocalFile();
1333 (currentSandboxURL.scheme() != "file" ||1330 if (disallowOutsideFiles && !PathUtils::isDescendantOf(thisURL, currentSandboxURL)) {
1334 !thisURL.toString(strippingFlags).startsWith(currentSandboxURL.toString(strippingFlags), getSensitivity()))) {
1335
1336 scriptWarningMessage("Script.include() ignoring file path" + thisURL.toString() 1331 scriptWarningMessage("Script.include() ignoring file path" + thisURL.toString()
1337 + "outside of original entity script" + currentSandboxURL.toString());1332 + "outside of original entity script" + currentSandboxURL.toString());
1338 } else {1333 } else {
@@ -1368,7 +1363,17 @@ void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callbac · 29 unmodified lines
1368 evaluate(contents, url.toString());1363 evaluate(contents, url.toString());
1369 };1364 };
1370 1365
1366 ExceptionEmitter tryCatch(this, "include");
1371 doWithEnvironment(capturedEntityIdentifier, capturedSandboxURL, operation);1367 doWithEnvironment(capturedEntityIdentifier, capturedSandboxURL, operation);
1368 if (tryCatch.hasPending()) {
1369 // match previous include behavior where possible by logging, not propagating, exceptions.
1370 // for nested evaluations, exceptions are now allowed to propagate
1371 auto err = tryCatch.pending();
1372 emit unhandledException(err);
1373 if (tryCatch.wouldEmit()) {
1374 tryCatch.consume();
1375 }
1376 }
1372 } else {1377 } else {
1373 scriptWarningMessage("Script.include() skipping evaluation of previously included url:" + url.toString());1378 scriptWarningMessage("Script.include() skipping evaluation of previously included url:" + url.toString());
1374 }1379 }
@@ -1470,10 +1475,16 @@ int ScriptEngine::getNumRunningEntityScripts() const { · 95 unmodified lines
1470 return sum;1475 return sum;
1471}1476}
1472 1477
1473QString ScriptEngine::getEntityScriptStatus(const EntityItemID& entityID) {1478void ScriptEngine::setEntityScriptDetails(const EntityItemID& entityID, const EntityScriptDetails& details) {
1474 if (_entityScripts.contains(entityID))1479 _entityScripts[entityID] = details;
1475 return EntityScriptStatus_::valueToKey(_entityScripts[entityID].status).toLower();1480 emit entityScriptDetailsUpdated();
1476 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();
1477}1488}
1478 1489
1479bool ScriptEngine::getEntityScriptDetails(const EntityItemID& entityID, EntityScriptDetails &details) const {1490bool ScriptEngine::getEntityScriptDetails(const EntityItemID& entityID, EntityScriptDetails &details) const {
@@ -1485,43 +1496,131 @@ bool ScriptEngine::getEntityScriptDetails(const EntityItemID& entityID, EntitySc · 5 unmodified lines
1485 return true;1496 return true;
1486}1497}
1487 1498
1488void ScriptEngine::setEntityScriptDetails(const EntityItemID& entityID, const EntityScriptDetails& details) {1499const auto RETRY_ATTEMPT_BATCH = 10; //glm::clamp(qgetenv("RETRY_ATTEMPT_BATCH").toInt(), 1, 50);
1489 _entityScripts[entityID] = details;1500const auto RETRY_ATTEMPT_MSECS = 250; //glm::clamp(qgetenv("RETRY_ATTEMPT_MSECS").toInt(), 50, 1000);
1490 emit entityScriptDetailsUpdated();
1491}
1492 1501
1493void ScriptEngine::updateEntityScriptStatus(const EntityItemID& entityID, const EntityScriptStatus &status, const QString& errorInfo) {1502void ScriptEngine::deferLoadEntityScript(const EntityItemID& entityID, const QString& entityScript, bool forceRedownload) {
1494 EntityScriptDetails &details = _entityScripts[entityID];1503 // forceRedownload doesn't make sense yet in this scenario (which only happens when another entity has taken lead on entityScript)
1495 details.status = status;1504 if (forceRedownload) {
1496 details.errorInfo = errorInfo;1505 qCDebug(scriptengine) << "deferLoadEntityScript -- TODO: ignoring forceRedownload =" << forceRedownload;
1497 emit entityScriptDetailsUpdated();1506 }
1498}
1499 1507
1500// since all of these operations can be asynch we will always do the actual work in the response handler1508 const auto needTimer = _pendingLoadEntities.isEmpty();
1501// for the download1509 _pendingLoadEntities.push_back({ entityID, entityScript/*, forceRedownload*/ });
1502void ScriptEngine::loadEntityScript(QWeakPointer<ScriptEngine> theEngine, const EntityItemID& entityID, const QString& entityScript, bool forceRedownload) {1510
1503 auto engine = theEngine.data();1511 auto processDeferredEntities = [this](QScriptContext* ctx, QScriptEngine*) -> QScriptValue {
1504 engine->executeOnScriptThread([=]{1512 //qCDebug(scriptengine) << "deferLoadEntityScript.retry #" << _pendingLoadEntities.size() << " pending loads | " << QThread::currentThread();
1505 EntityScriptDetails details = engine->_entityScripts[entityID];1513
1506 if (details.status == EntityScriptStatus::PENDING || details.status == EntityScriptStatus::UNLOADED) {1514 for(int i=0; i < RETRY_ATTEMPT_BATCH && !_pendingLoadEntities.isEmpty(); i++) {
1507 engine->updateEntityScriptStatus(entityID, EntityScriptStatus::LOADING, QThread::currentThread()->objectName());1515 auto retry = _pendingLoadEntities.takeFirst();
1516 qCDebug(scriptengine) << "deferLoadEntityScript.retry[" << i << "]:" << retry.entityID << retry.entityScript;
1517 if(!_entityScripts.contains(retry.entityID)) {
1518 qCDebug(scriptengine) << "deferLoadEntityScript.retry -- entity details gone (entity deleted?)";
1519 } else {
1520 auto details = _entityScripts[retry.entityID];
1521 if (details.status != EntityScriptStatus::PENDING) {
1522 qCDebug(scriptengine) << "deferLoadEntityScript.retry -- entity status no longer PENDING; " << details.status;
1523 } else {
1524 loadEntityScript(retry.entityID, retry.entityScript, false);
1525 }
1526 }
1508 }1527 }
1509 });1528 if (_pendingLoadEntities.isEmpty()) {
1529 QObject *interval = ctx->callee().property("interval").toQObject();
1530 // we're done processing so can kill the timer
1531 qCDebug(scriptengine) << "loadEntityScript.retry queue processing complete -- exiting" << interval;
1532 if (interval) {
1533 clearInterval(interval);
1534 }
1535 }
1536 return QScriptValue();
1537 };
1510 1538
1511 // NOTE: If the script content is not currently in the cache, the LAMBDA here will be called on the Main Thread1539 if (needTimer) {
1512 // which means we're guaranteed that it's not the correct thread for the ScriptEngine. This means1540 // we were the first to get queued -- so set up an interval timer for deferred processing
1513 // when we get into entityScriptContentAvailable() we will likely invokeMethod() to get it over1541 qCDebug(scriptengine) << "deferLoadEntityScript -- scheduling timer " << RETRY_ATTEMPT_MSECS;
1514 // to the "Entities" ScriptEngine thread.1542
1515 DependencyManager::get<ScriptCache>()->getScriptContents(entityScript, [theEngine, entityID](const QString& scriptOrURL, const QString& contents, bool isURL, bool success, const QString &status) {1543 // processDeferredEntities gets wrapped as a "regular" JavaScript callback so that it can then
1516 QSharedPointer<ScriptEngine> strongEngine = theEngine.toStrongRef();1544 // be scheduled using setInterval per usual -- allowing the script engine to own its lifecycle.
1517 if (strongEngine) {1545 // we stil need to call clearInterval manually when done, so its passed to calle() above as a property.
1518#ifdef THREAD_DEBUGGING1546
1519 qCDebug(scriptengine) << "ScriptEngine::entityScriptContentAvailable() IN LAMBDA contentAvailable on thread ["1547 QScriptValue function = newLambdaFunction(processDeferredEntities, "processDeferredEntities-loader");
1520 << QThread::currentThread() << "] expected thread [" << strongEngine->thread() << "]";1548 QObject *interval = setInterval( function, RETRY_ATTEMPT_MSECS );
1549
1550 // make the interval reachable from the handler
1551 function.setProperty("interval", newQObject(interval));
1552 }
1553}
1554
1555void ScriptEngine::loadEntityScript(const EntityItemID& entityID, const QString& entityScript, bool forceRedownload) {
1556 if (QThread::currentThread() != thread()) {
1557 QMetaObject::invokeMethod(this, "loadEntityScript",
1558 Q_ARG(const EntityItemID&, entityID),
1559 Q_ARG(const QString&, entityScript),
1560 Q_ARG(bool, forceRedownload)
1561 );
1562 return;
1563 }
1564 PROFILE_RANGE(script, __FUNCTION__);
1565
1566 if (isStopping() || DependencyManager::get<ScriptEngines>()->isStopped()) {
1567 qCDebug(scriptengine) << "loadEntityScript.start " << entityScript << entityID.toString()
1568 << " but isStopping==" << isStopping()
1569 << " || engines->isStopped==" << DependencyManager::get<ScriptEngines>()->isStopped();
1570 return;
1571 }
1572
1573 if (!_entityScripts.contains(entityID)) {
1574 // make sure EntityScriptDetails has an entry for this UUID right away
1575 // (which allows bailing from the loading/provisioning process early if the Entity gets deleted mid-flight)
1576 updateEntityScriptStatus(entityID, EntityScriptStatus::PENDING, "...pending...");
1577 }
1578
1579 // This lock/defer approach allows multiple Entities to boot from the same script URL while still taking
1580 // full advantage of cacheable require modules. This only affects Entities literally coming in back-to-back
1581 // before the first one has time to finish loading.
1582 auto &locker = _lockPerUniqueScriptURL[entityScript];
1583 if (!locker.tryLock()) {
1584 qCDebug(scriptengine) << QString("loadEntityScript.deferring[%1] entity: %2 script: %3")
1585 .arg( _pendingLoadEntities.size()).arg(entityID.toString()).arg(entityScript);
1586 deferLoadEntityScript(entityID, entityScript, forceRedownload);
1587 return;
1588 }
1589
1590#ifdef DEBUG_ENTITY_STATES
1591 auto previousStatus = _entityScripts.contains(entityID) ? _entityScripts[entityID].status : EntityScriptStatus::PENDING;
1592 qCDebug(scriptengine) << "loadEntityScript.LOADING: " << entityScript << entityID.toString()
1593 << "(previous: " << previousStatus << ")";
1521#endif1594#endif
1522 strongEngine->entityScriptContentAvailable(entityID, scriptOrURL, contents, isURL, success, status);1595
1523 }1596 EntityScriptDetails newDetails;
1524 }, forceRedownload, processLevelMaxRetries);1597 newDetails.scriptText = entityScript;
1598 newDetails.status = EntityScriptStatus::LOADING;
1599 newDetails.definingSandboxURL = currentSandboxURL;
1600 _entityScripts[entityID] = newDetails;
1601
1602 auto scriptCache = DependencyManager::get<ScriptCache>();
1603 scriptCache->getScriptContents(entityScript,
1604 [=, &locker](const QString& url, const QString& contents, bool isURL, bool success, const QString& status) {
1605 if (isStopping()) {
1606#ifdef DEBUG_ENTITY_STATES
1607 qCDebug(scriptengine) << "loadEntityScript.contentAvailable -- stopping";
1608#endif
1609 locker.unlock();
1610 return;
1611 }
1612 executeOnScriptThread([=, &locker]{
1613 //qCDebug(scriptengine) << "loadEntityScript.contentAvailable" << status << QUrl(url).fileName() << entityID.toString();
1614 if (!isStopping() && _entityScripts.contains(entityID)) {
1615 entityScriptContentAvailable(entityID, url, contents, isURL, success, status);
1616 } else {
1617#ifdef DEBUG_ENTITY_STATES
1618 qCDebug(scriptengine) << "loadEntityScript.contentAvailable -- aborting";
1619#endif
1620 }
1621 locker.unlock();
1622 });
1623 }, forceRedownload);
1525}1624}
1526 1625
1527// since all of these operations can be asynch we will always do the actual work in the response handler1626// since all of these operations can be asynch we will always do the actual work in the response handler
@@ -1551,25 +1650,53 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co · 23 unmodified lines
1551 1650
1552 auto scriptCache = DependencyManager::get<ScriptCache>();1651 auto scriptCache = DependencyManager::get<ScriptCache>();
1553 bool isFileUrl = isURL && scriptOrURL.startsWith("file://");1652 bool isFileUrl = isURL && scriptOrURL.startsWith("file://");
1554 auto fileName = isURL ? scriptOrURL : "EmbeddedEntityScript";1653 auto fileName = isURL ? scriptOrURL : "about:EmbeddedEntityScript";
1555 1654
1655 const EntityScriptDetails &oldDetails = _entityScripts[entityID];
1556 EntityScriptDetails newDetails;1656 EntityScriptDetails newDetails;
1557 newDetails.scriptText = scriptOrURL;1657 newDetails.scriptText = scriptOrURL;
1558 1658
1559 if (!success) {1659 // If an error happens below, we want to update newDetails with the new status info
1560 newDetails.status = EntityScriptStatus::ERROR_LOADING_SCRIPT;1660 // and also abort any pending Entity loads that are waiting on the exact same script URL.
1561 newDetails.errorInfo = "Failed to load script (" + status + ")";1661 auto setError = [&](const QString &errorInfo, const EntityScriptStatus& status) {
1662 newDetails.errorInfo = errorInfo;
1663 newDetails.status = status;
1562 setEntityScriptDetails(entityID, newDetails);1664 setEntityScriptDetails(entityID, newDetails);
1665
1666 QMutableListIterator<DeferredLoadEntity> i(_pendingLoadEntities);
1667 // note: we want the *original* entityScript string (not the potentially normalized one from ScriptCache results)
1668 const auto entityScript = oldDetails.scriptText;
1669 while (i.hasNext()) {
1670 auto retry = i.next();
1671 if (retry.entityScript == entityScript) {
1672 qCDebug(scriptengine) << "... pending load of " << retry.entityID << " cancelled";
1673 i.remove();
1674 }
1675 }
1676 };
1677
1678 // NETWORK / FILESYSTEM ERRORS
1679 if (!success) {
1680 setError("Failed to load script (" + status + ")", EntityScriptStatus::ERROR_LOADING_SCRIPT);
1563 return;1681 return;
1564 }1682 }
1565 1683
1684 // SYNTAX ERRORS
1566 auto syntaxError = lintScript(contents, fileName);1685 auto syntaxError = lintScript(contents, fileName);
1686 if (syntaxError.isError()) {
1687 auto message = syntaxError.property("formatted").toString();
1688 if (message.isEmpty()) {
1689 message = syntaxError.toString();
1690 }
1691 setError(QString("Bad syntax (%1)").arg(message), EntityScriptStatus::ERROR_RUNNING_SCRIPT);
1692 syntaxError.setProperty("detail", entityID.toString());
1693 emit unhandledException(syntaxError);
1694 return;
1695 }
1567 QScriptProgram program { contents, fileName };1696 QScriptProgram program { contents, fileName };
1568 if (!syntaxError.isNull() || program.isNull()) {1697 if (program.isNull()) {
1569 newDetails.status = EntityScriptStatus::ERROR_RUNNING_SCRIPT;1698 setError("Bad program (isNull)", EntityScriptStatus::ERROR_RUNNING_SCRIPT);
1570 newDetails.errorInfo = QString("Bad syntax (%1)").arg(syntaxError);1699 emit unhandledException(makeError("program.isNull"));
1571 setEntityScriptDetails(entityID, newDetails);
1572 qCDebug(scriptengine) << newDetails.errorInfo << scriptOrURL;
1573 return; // done processing script1700 return; // done processing script
1574 }1701 }
1575 1702
@@ -1577,11 +1704,13 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co · 1 unmodified line
1577 setParentURL(scriptOrURL);1704 setParentURL(scriptOrURL);
1578 }1705 }
1579 1706
1707 // SANITY/PERFORMANCE CHECK USING SANDBOX
1580 const int SANDBOX_TIMEOUT = 0.25 * MSECS_PER_SECOND;1708 const int SANDBOX_TIMEOUT = 0.25 * MSECS_PER_SECOND;
1581 BaseScriptEngine sandbox;1709 BaseScriptEngine sandbox;
1582 sandbox.setProcessEventsInterval(SANDBOX_TIMEOUT);1710 sandbox.setProcessEventsInterval(SANDBOX_TIMEOUT);
1583 QScriptValue testConstructor;1711 QScriptValue testConstructor, exception;
1584 {1712 {
1713 ExceptionEmitter tryCatch(&sandbox, QString("(preflight %1)").arg(entityID.toString()));
1585 QTimer timeout;1714 QTimer timeout;
1586 timeout.setSingleShot(true);1715 timeout.setSingleShot(true);
1587 timeout.start(SANDBOX_TIMEOUT);1716 timeout.start(SANDBOX_TIMEOUT);
@@ -1594,18 +1723,25 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co · 6 unmodified lines
1594 context->throwError(QString("Timed out (entity constructors are limited to %1ms)").arg(SANDBOX_TIMEOUT));1723 context->throwError(QString("Timed out (entity constructors are limited to %1ms)").arg(SANDBOX_TIMEOUT));
1595 }1724 }
1596 });1725 });
1726
1597 testConstructor = sandbox.evaluate(program);1727 testConstructor = sandbox.evaluate(program);
1728
1729 if (tryCatch.hasPending()) {
1730 exception = tryCatch.consume();
1731 } else if (testConstructor.isError()) {
1732 exception = testConstructor;
1733 }
1598 }1734 }
1599 1735
1600 QString exceptionMessage = sandbox.formatUncaughtException(program.fileName());1736 if (exception.isError()) {
1601 if (!exceptionMessage.isNull()) {1737 // create a local copy using makeError to decouple from the sandbox engine
1602 newDetails.status = EntityScriptStatus::ERROR_RUNNING_SCRIPT;1738 exception = makeError(exception);
1603 newDetails.errorInfo = exceptionMessage;1739 setError(formatException(exception), EntityScriptStatus::ERROR_RUNNING_SCRIPT);
1604 setEntityScriptDetails(entityID, newDetails);1740 emit unhandledException(exception);
1605 qCDebug(scriptengine) << "----- ScriptEngine::entityScriptContentAvailable -- hadUncaughtExceptions (" << scriptOrURL << ")";
1606 return;1741 return;
1607 }1742 }
1608 1743
1744 // CONSTRUCTOR VIABILITY
1609 if (!testConstructor.isFunction()) {1745 if (!testConstructor.isFunction()) {
1610 QString testConstructorType = QString(testConstructor.toVariant().typeName());1746 QString testConstructorType = QString(testConstructor.toVariant().typeName());
1611 if (testConstructorType == "") {1747 if (testConstructorType == "") {
@@ -1616,32 +1752,49 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co · 4 unmodified lines
1616 if (testConstructorValue.size() > maxTestConstructorValueSize) {1752 if (testConstructorValue.size() > maxTestConstructorValueSize) {
1617 testConstructorValue = testConstructorValue.mid(0, maxTestConstructorValueSize) + "...";1753 testConstructorValue = testConstructorValue.mid(0, maxTestConstructorValueSize) + "...";
1618 }1754 }
1619 scriptErrorMessage("Error -- ScriptEngine::loadEntityScript() entity:" + entityID.toString()1755 auto message = QString("failed to load entity script -- expected a function, got %1, %2")
1620 + "failed to load entity script -- expected a function, got " + testConstructorType1756 .arg(testConstructorType).arg(testConstructorValue);
1621 + "," + testConstructorValue
1622 + "," + scriptOrURL);
1623 1757
1624 newDetails.status = EntityScriptStatus::ERROR_RUNNING_SCRIPT;1758 auto err = makeError(message);
1625 newDetails.errorInfo = "Could not find constructor";1759 err.setProperty("fileName", scriptOrURL);
1626 setEntityScriptDetails(entityID, newDetails);1760 err.setProperty("detail", "(constructor " + entityID.toString() + ")");
1627 1761
1628 qCDebug(scriptengine) << "----- ScriptEngine::entityScriptContentAvailable -- failed to run (" << scriptOrURL << ")";1762 setError("Could not find constructor (" + testConstructorType + ")", EntityScriptStatus::ERROR_RUNNING_SCRIPT);
1763 emit unhandledException(err);
1629 return; // done processing script1764 return; // done processing script
1630 }1765 }
1631 1766
1767 // (this feeds into refreshFileScript)
1632 int64_t lastModified = 0;1768 int64_t lastModified = 0;
1633 if (isFileUrl) {1769 if (isFileUrl) {
1634 QString file = QUrl(scriptOrURL).toLocalFile();1770 QString file = QUrl(scriptOrURL).toLocalFile();
1635 lastModified = (quint64)QFileInfo(file).lastModified().toMSecsSinceEpoch();1771 lastModified = (quint64)QFileInfo(file).lastModified().toMSecsSinceEpoch();
1636 }1772 }
1773
1774 // THE ACTUAL EVALUATION AND CONSTRUCTION
1637 QScriptValue entityScriptConstructor, entityScriptObject;1775 QScriptValue entityScriptConstructor, entityScriptObject;
1638 QUrl sandboxURL = currentSandboxURL.isEmpty() ? scriptOrURL : currentSandboxURL;1776 QUrl sandboxURL = currentSandboxURL.isEmpty() ? scriptOrURL : currentSandboxURL;
1639 auto initialization = [&]{1777 auto initialization = [&]{
1778 ExceptionEmitter tryCatch(this, "(construct " + entityID.toString() + ")");
1779
1640 entityScriptConstructor = evaluate(contents, fileName);1780 entityScriptConstructor = evaluate(contents, fileName);
1641 entityScriptObject = entityScriptConstructor.construct();1781 entityScriptObject = entityScriptConstructor.construct();
1782
1783 if (tryCatch.hasPending()) {
1784 entityScriptObject = tryCatch.consume();
1785 }
1642 };1786 };
1787
1643 doWithEnvironment(entityID, sandboxURL, initialization);1788 doWithEnvironment(entityID, sandboxURL, initialization);
1644 1789
1790 if (entityScriptObject.isError()) {
1791 auto exception = entityScriptObject;
1792 setError(formatException(exception), EntityScriptStatus::ERROR_RUNNING_SCRIPT);
1793 emit unhandledException(exception);
1794 return;
1795 }
1796
1797 // ... AND WE HAVE LIFTOFF
1645 newDetails.status = EntityScriptStatus::RUNNING;1798 newDetails.status = EntityScriptStatus::RUNNING;
1646 newDetails.scriptObject = entityScriptObject;1799 newDetails.scriptObject = entityScriptObject;
1647 newDetails.lastModified = lastModified;1800 newDetails.lastModified = lastModified;
@@ -1678,6 +1831,7 @@ void ScriptEngine::unloadEntityScript(const EntityItemID& entityID) { · 30 unmodified lines
1678 }1831 }
1679 EntityScriptDetails newDetails;1832 EntityScriptDetails newDetails;
1680 newDetails.status = EntityScriptStatus::UNLOADED;1833 newDetails.status = EntityScriptStatus::UNLOADED;
1834 newDetails.lastModified = QDateTime::currentMSecsSinceEpoch();
1681 setEntityScriptDetails(entityID, newDetails);1835 setEntityScriptDetails(entityID, newDetails);
1682 stopAllTimersForEntityScript(entityID);1836 stopAllTimersForEntityScript(entityID);
1683 }1837 }
@@ -1700,15 +1854,14 @@ void ScriptEngine::unloadAllEntityScripts() { · 16 unmodified lines
1700 }1854 }
1701 _entityScripts.clear();1855 _entityScripts.clear();
1702 emit entityScriptDetailsUpdated();1856 emit entityScriptDetailsUpdated();
1857 _lockPerUniqueScriptURL.clear();
1703 1858
1704#ifdef DEBUG_ENGINE_STATE1859#ifdef DEBUG_ENGINE_STATE
1705 qCDebug(scriptengine) << "---- CURRENT STATE OF ENGINE: --------------------------";1860 _debugDump(
1706 QScriptValueIterator it(globalObject());1861 "---- CURRENT STATE OF ENGINE: --------------------------",
1707 while (it.hasNext()) {1862 globalObject(),
1708 it.next();1863 "--------------------------------------------------------"
1709 qCDebug(scriptengine) << it.name() << ":" << it.value().toString();1864 );
1710 }
1711 qCDebug(scriptengine) << "--------------------------------------------------------";
1712#endif // DEBUG_ENGINE_STATE1865#endif // DEBUG_ENGINE_STATE
1713}1866}
1714 1867
@@ -1756,6 +1909,8 @@ void ScriptEngine::doWithEnvironment(const EntityItemID& entityID, const QUrl& s · 41 unmodified lines
1756 currentEntityIdentifier = entityID;1909 currentEntityIdentifier = entityID;
1757 currentSandboxURL = sandboxURL;1910 currentSandboxURL = sandboxURL;
1758 1911
1912 ExceptionEmitter tryCatcher(this, !entityID.isNull() ? entityID.toString() : __FUNCTION__);
1913
1759#if DEBUG_CURRENT_ENTITY1914#if DEBUG_CURRENT_ENTITY
1760 QScriptValue oldData = this->globalObject().property("debugEntityID");1915 QScriptValue oldData = this->globalObject().property("debugEntityID");
1761 this->globalObject().setProperty("debugEntityID", entityID.toScriptValue(this)); // Make the entityID available to javascript as a global.1916 this->globalObject().setProperty("debugEntityID", entityID.toScriptValue(this)); // Make the entityID available to javascript as a global.
@@ -1764,14 +1919,10 @@ void ScriptEngine::doWithEnvironment(const EntityItemID& entityID, const QUrl& s · 2 unmodified lines
1764#else1919#else
1765 operation();1920 operation();
1766#endif1921#endif
1767 if (hasUncaughtException()) {
1768 reportUncaughtException();
1769 clearExceptions();
1770 }
1771
1772 currentEntityIdentifier = oldIdentifier;1922 currentEntityIdentifier = oldIdentifier;
1773 currentSandboxURL = oldSandboxURL;1923 currentSandboxURL = oldSandboxURL;
1774}1924}
1925
1775void ScriptEngine::callWithEnvironment(const EntityItemID& entityID, const QUrl& sandboxURL, QScriptValue function, QScriptValue thisObject, QScriptValueList args) {1926void ScriptEngine::callWithEnvironment(const EntityItemID& entityID, const QUrl& sandboxURL, QScriptValue function, QScriptValue thisObject, QScriptValueList args) {
1776 auto operation = [&]() {1927 auto operation = [&]() {
1777 function.call(thisObject, args);1928 function.call(thisObject, args);
@@ -1846,7 +1997,6 @@ void ScriptEngine::callEntityScriptMethod(const EntityItemID& entityID, const QS · 68 unmodified lines
1846 }1997 }
1847}1998}
1848 1999
1849
1850void ScriptEngine::callEntityScriptMethod(const EntityItemID& entityID, const QString& methodName, const EntityItemID& otherID, const Collision& collision) {2000void ScriptEngine::callEntityScriptMethod(const EntityItemID& entityID, const QString& methodName, const EntityItemID& otherID, const Collision& collision) {
1851 if (QThread::currentThread() != thread()) {2001 if (QThread::currentThread() != thread()) {
1852#ifdef THREAD_DEBUGGING2002#ifdef THREAD_DEBUGGING
@@ -1881,3 +2031,4 @@ void ScriptEngine::callEntityScriptMethod(const EntityItemID& entityID, const QS · 28 unmodified lines
1881 }2031 }
1882 }2032 }
1883}2033}
2034