History

* Update per 21114-part2 changes. * Add explicit thread safety guards. * Add Entities.queryPropertyMetdata for unit testing Entity script module support. * Cleanup / commenting pass.

humbletim committed 9 years ago

Commit 40ba8181 parent 9b096518 files · +439 −158Browse files

Showing the changes to libraries/shared/src/BaseScriptEngine.cpp (1 of 8 files). Show all

Unified · Split

libraries/shared/src/BaseScriptEngine.cpp+101−28
@@ -10,6 +10,7 @@ · 9 unmodified lines
1010 //
1111
1212 #include "BaseScriptEngine.h"
13+#include "SharedLogging.h"
1314
1415 #include <QtCore/QString>
1516 #include <QtCore/QThread>
@@ -18,18 +19,26 @@ · 2 unmodified lines
1819 #include <QtScript/QScriptValueIterator>
1920 #include <QtScript/QScriptContextInfo>
2021
21−#include "ScriptEngineLogging.h"
2222 #include "Profile.h"
2323
24−const QString BaseScriptEngine::_SETTINGS_ENABLE_EXTENDED_EXCEPTIONS {
25− "com.highfidelity.experimental.enableExtendedJSExceptions"
26−};
27−
2824 const QString BaseScriptEngine::SCRIPT_EXCEPTION_FORMAT { "[%0] %1 in %2:%3" };
2925 const QString BaseScriptEngine::SCRIPT_BACKTRACE_SEP { "\n " };
3026
27+bool BaseScriptEngine::IS_THREADSAFE_INVOCATION(const QThread *thread, const QString& method) {
28+ if (QThread::currentThread() == thread) {
29+ return true;
30+ }
31+ qCCritical(shared) << QString("Scripting::%1 @ %2 -- ignoring thread-unsafe call from %3")
32+ .arg(method).arg(thread ? thread->objectName() : "(!thread)").arg(QThread::currentThread()->objectName());
33+ qCDebug(shared) << "(please resolve on the calling side by using invokeMethod, executeOnScriptThread, etc.)";
34+ return false;
35+}
36+
3137 // engine-aware JS Error copier and factory
3238 QScriptValue BaseScriptEngine::makeError(const QScriptValue& _other, const QString& type) {
39+ if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
40+ return unboundNullValue();
41+ }
3342 auto other = _other;
3443 if (other.isString()) {
3544 other = newObject();
@@ -41,7 +50,7 @@ QScriptValue BaseScriptEngine::makeError(const QScriptValue& _other, const QStri · 5 unmodified lines
4150 }
4251 if (!proto.isFunction()) {
4352 #ifdef DEBUG_JS_EXCEPTIONS
44− qCDebug(scriptengine) << "BaseScriptEngine::makeError -- couldn't find constructor for" << type << " -- using Error instead";
53+ qCDebug(shared) << "BaseScriptEngine::makeError -- couldn't find constructor for" << type << " -- using Error instead";
4554 #endif
4655 proto = globalObject().property("Error");
4756 }
@@ -64,6 +73,9 @@ QScriptValue BaseScriptEngine::makeError(const QScriptValue& _other, const QStri · 16 unmodified lines
6473
6574 // check syntax and when there are issues returns an actual "SyntaxError" with the details
6675 QScriptValue BaseScriptEngine::lintScript(const QString& sourceCode, const QString& fileName, const int lineNumber) {
76+ if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
77+ return unboundNullValue();
78+ }
6779 const auto syntaxCheck = checkSyntax(sourceCode);
6880 if (syntaxCheck.state() != QScriptSyntaxCheckResult::Valid) {
6981 auto err = globalObject().property("SyntaxError")
@@ -82,13 +94,16 @@ QScriptValue BaseScriptEngine::lintScript(const QString& sourceCode, const QStri · 12 unmodified lines
8294 }
8395 return err;
8496 }
85− return undefinedValue();
97+ return QScriptValue();
8698 }
8799
88100 // this pulls from the best available information to create a detailed snapshot of the current exception
89101 QScriptValue BaseScriptEngine::cloneUncaughtException(const QString& extraDetail) {
102+ if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
103+ return unboundNullValue();
104+ }
90105 if (!hasUncaughtException()) {
91− return QScriptValue();
106+ return unboundNullValue();
92107 }
93108 auto exception = uncaughtException();
94109 // ensure the error object is engine-local
@@ -144,7 +159,10 @@ QScriptValue BaseScriptEngine::cloneUncaughtException(const QString& extraDetail · 49 unmodified lines
144159 return err;
145160 }
146161
147−QString BaseScriptEngine::formatException(const QScriptValue& exception) {
162+QString BaseScriptEngine::formatException(const QScriptValue& exception, bool includeExtendedDetails) {
163+ if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
164+ return QString();
165+ }
148166 QString note { "UncaughtException" };
149167 QString result;
150168
@@ -156,8 +174,8 @@ QString BaseScriptEngine::formatException(const QScriptValue& exception) { · 5 unmodified lines
156174 const auto lineNumber = exception.property("lineNumber").toString();
157175 const auto stacktrace = exception.property("stack").toString();
158176
159− if (_enableExtendedJSExceptions.get()) {
160− // This setting toggles display of the hints now being added during the loading process.
177+ if (includeExtendedDetails) {
178+ // Display additional exception / troubleshooting hints that can be added via the custom Error .detail property
161179 // Example difference:
162180 // [UncaughtExceptions] Error: Can't find variable: foobar in atp:/myentity.js\n...
163181 // [UncaughtException (construct {1eb5d3fa-23b1-411c-af83-163af7220e3f})] Error: Can't find variable: foobar in atp:/myentity.js\n...
@@ -173,14 +191,39 @@ QString BaseScriptEngine::formatException(const QScriptValue& exception) { · 9 unmodified lines
173191 return result;
174192 }
175193
194+bool BaseScriptEngine::raiseException(const QScriptValue& exception) {
195+ if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
196+ return false;
197+ }
198+ if (currentContext()) {
199+ // we have an active context / JS stack frame so throw the exception per usual
200+ currentContext()->throwValue(makeError(exception));
201+ return true;
202+ } else {
203+ // we are within a pure C++ stack frame (ie: being called directly by other C++ code)
204+ // in this case no context information is available so just emit the exception for reporting
205+ emit unhandledException(makeError(exception));
206+ }
207+ return false;
208+}
209+
210+bool BaseScriptEngine::maybeEmitUncaughtException(const QString& debugHint) {
211+ if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
212+ return false;
213+ }
214+ if (!isEvaluating() && hasUncaughtException()) {
215+ emit unhandledException(cloneUncaughtException(debugHint));
216+ clearExceptions();
217+ return true;
218+ }
219+ return false;
220+}
221+
176222 QScriptValue BaseScriptEngine::evaluateInClosure(const QScriptValue& closure, const QScriptProgram& program) {
177223 PROFILE_RANGE(script, "evaluateInClosure");
178− if (QThread::currentThread() != thread()) {
179− qCCritical(scriptengine) << "*** CRITICAL *** ScriptEngine::evaluateInClosure() is meant to be called from engine thread only.";
180− // note: a recursive mutex might be needed around below code if this method ever becomes Q_INVOKABLE
181− return QScriptValue();
224+ if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
225+ return unboundNullValue();
182226 }
183−
184227 const auto fileName = program.fileName();
185228 const auto shortName = QUrl(fileName).fileName();
186229
@@ -189,7 +232,7 @@ QScriptValue BaseScriptEngine::evaluateInClosure(const QScriptValue& closure, co · 2 unmodified lines
189232 auto global = closure.property("global");
190233 if (global.isObject()) {
191234 #ifdef DEBUG_JS
192− qCDebug(scriptengine) << " setting global = closure.global" << shortName;
235+ qCDebug(shared) << " setting global = closure.global" << shortName;
193236 #endif
194237 oldGlobal = globalObject();
195238 setGlobalObject(global);
@@ -200,34 +243,34 @@ QScriptValue BaseScriptEngine::evaluateInClosure(const QScriptValue& closure, co · 4 unmodified lines
200243 auto thiz = closure.property("this");
201244 if (thiz.isObject()) {
202245 #ifdef DEBUG_JS
203− qCDebug(scriptengine) << " setting this = closure.this" << shortName;
246+ qCDebug(shared) << " setting this = closure.this" << shortName;
204247 #endif
205248 context->setThisObject(thiz);
206249 }
207250
208251 context->pushScope(closure);
209252 #ifdef DEBUG_JS
210− qCDebug(scriptengine) << QString("[%1] evaluateInClosure %2").arg(isEvaluating()).arg(shortName);
253+ qCDebug(shared) << QString("[%1] evaluateInClosure %2").arg(isEvaluating()).arg(shortName);
211254 #endif
212255 {
213256 result = BaseScriptEngine::evaluate(program);
214257 if (hasUncaughtException()) {
215258 auto err = cloneUncaughtException(__FUNCTION__);
216259 #ifdef DEBUG_JS_EXCEPTIONS
217− qCWarning(scriptengine) << __FUNCTION__ << "---------- hasCaught:" << err.toString() << result.toString();
260+ qCWarning(shared) << __FUNCTION__ << "---------- hasCaught:" << err.toString() << result.toString();
218261 err.setProperty("_result", result);
219262 #endif
220263 result = err;
221264 }
222265 }
223266 #ifdef DEBUG_JS
224− qCDebug(scriptengine) << QString("[%1] //evaluateInClosure %2").arg(isEvaluating()).arg(shortName);
267+ qCDebug(shared) << QString("[%1] //evaluateInClosure %2").arg(isEvaluating()).arg(shortName);
225268 #endif
226269 popContext();
227270
228271 if (oldGlobal.isValid()) {
229272 #ifdef DEBUG_JS
230− qCDebug(scriptengine) << " restoring global" << shortName;
273+ qCDebug(shared) << " restoring global" << shortName;
231274 #endif
232275 setGlobalObject(oldGlobal);
233276 }
@@ -236,7 +279,6 @@ QScriptValue BaseScriptEngine::evaluateInClosure(const QScriptValue& closure, co · 2 unmodified lines
236279 }
237280
238281 // Lambda
239−
240282 QScriptValue BaseScriptEngine::newLambdaFunction(std::function<QScriptValue(QScriptContext *, QScriptEngine*)> operation, const QScriptValue& data, const QScriptEngine::ValueOwnership& ownership) {
241283 auto lambda = new Lambda(this, operation, data);
242284 auto object = newQObject(lambda, ownership);
@@ -262,26 +304,57 @@ Lambda::Lambda(QScriptEngine *engine, std::function<QScriptValue(QScriptContext · 19 unmodified lines
262304 #endif
263305 }
264306 QScriptValue Lambda::call() {
307+ if (!BaseScriptEngine::IS_THREADSAFE_INVOCATION(engine->thread(), __FUNCTION__)) {
308+ return BaseScriptEngine::unboundNullValue();
309+ }
265310 return operation(engine->currentContext(), engine);
266311 }
267312
313+QScriptValue makeScopedHandlerObject(QScriptValue scopeOrCallback, QScriptValue methodOrName) {
314+ auto engine = scopeOrCallback.engine();
315+ if (!engine) {
316+ return scopeOrCallback;
317+ }
318+ auto scope = QScriptValue();
319+ auto callback = scopeOrCallback;
320+ if (scopeOrCallback.isObject()) {
321+ if (methodOrName.isString()) {
322+ scope = scopeOrCallback;
323+ callback = scope.property(methodOrName.toString());
324+ } else if (methodOrName.isFunction()) {
325+ scope = scopeOrCallback;
326+ callback = methodOrName;
327+ }
328+ }
329+ auto handler = engine->newObject();
330+ handler.setProperty("scope", scope);
331+ handler.setProperty("callback", callback);
332+ return handler;
333+}
334+
335+QScriptValue callScopedHandlerObject(QScriptValue handler, QScriptValue err, QScriptValue result) {
336+ return handler.property("callback").call(handler.property("scope"), QScriptValueList({ err, result }));
337+}
338+
268339 #ifdef DEBUG_JS
269340 void BaseScriptEngine::_debugDump(const QString& header, const QScriptValue& object, const QString& footer) {
341+ if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
342+ return;
343+ }
270344 if (!header.isEmpty()) {
271− qCDebug(scriptengine) << header;
345+ qCDebug(shared) << header;
272346 }
273347 if (!object.isObject()) {
274− qCDebug(scriptengine) << "(!isObject)" << object.toVariant().toString() << object.toString();
348+ qCDebug(shared) << "(!isObject)" << object.toVariant().toString() << object.toString();
275349 return;
276350 }
277351 QScriptValueIterator it(object);
278352 while (it.hasNext()) {
279353 it.next();
280− qCDebug(scriptengine) << it.name() << ":" << it.value().toString();
354+ qCDebug(shared) << it.name() << ":" << it.value().toString();
281355 }
282356 if (!footer.isEmpty()) {
283− qCDebug(scriptengine) << footer;
357+ qCDebug(shared) << footer;
284358 }
285359 }
286360 #endif
287−