Showing the changes to libraries/shared/src/BaseScriptEngine.cpp (1 of 8 files). Show all
Unified · Split
| @@ -10,6 +10,7 @@ · 9 unmodified lines | ||
| 10 | 10 | // |
| 11 | 11 | |
| 12 | 12 | #include "BaseScriptEngine.h" |
| 13 | +#include "SharedLogging.h" | |
| 13 | 14 | |
| 14 | 15 | #include <QtCore/QString> |
| 15 | 16 | #include <QtCore/QThread> |
| @@ -18,18 +19,26 @@ · 2 unmodified lines | ||
| 18 | 19 | #include <QtScript/QScriptValueIterator> |
| 19 | 20 | #include <QtScript/QScriptContextInfo> |
| 20 | 21 | |
| 21 | −#include "ScriptEngineLogging.h" | |
| 22 | 22 | #include "Profile.h" |
| 23 | 23 | |
| 24 | −const QString BaseScriptEngine::_SETTINGS_ENABLE_EXTENDED_EXCEPTIONS { | |
| 25 | − "com.highfidelity.experimental.enableExtendedJSExceptions" | |
| 26 | −}; | |
| 27 | − | |
| 28 | 24 | const QString BaseScriptEngine::SCRIPT_EXCEPTION_FORMAT { "[%0] %1 in %2:%3" }; |
| 29 | 25 | const QString BaseScriptEngine::SCRIPT_BACKTRACE_SEP { "\n " }; |
| 30 | 26 | |
| 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 | + | |
| 31 | 37 | // engine-aware JS Error copier and factory |
| 32 | 38 | QScriptValue BaseScriptEngine::makeError(const QScriptValue& _other, const QString& type) { |
| 39 | + if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { | |
| 40 | + return unboundNullValue(); | |
| 41 | + } | |
| 33 | 42 | auto other = _other; |
| 34 | 43 | if (other.isString()) { |
| 35 | 44 | other = newObject(); |
| @@ -41,7 +50,7 @@ QScriptValue BaseScriptEngine::makeError(const QScriptValue& _other, const QStri · 5 unmodified lines | ||
| 41 | 50 | } |
| 42 | 51 | if (!proto.isFunction()) { |
| 43 | 52 | #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"; | |
| 45 | 54 | #endif |
| 46 | 55 | proto = globalObject().property("Error"); |
| 47 | 56 | } |
| @@ -64,6 +73,9 @@ QScriptValue BaseScriptEngine::makeError(const QScriptValue& _other, const QStri · 16 unmodified lines | ||
| 64 | 73 | |
| 65 | 74 | // check syntax and when there are issues returns an actual "SyntaxError" with the details |
| 66 | 75 | QScriptValue BaseScriptEngine::lintScript(const QString& sourceCode, const QString& fileName, const int lineNumber) { |
| 76 | + if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { | |
| 77 | + return unboundNullValue(); | |
| 78 | + } | |
| 67 | 79 | const auto syntaxCheck = checkSyntax(sourceCode); |
| 68 | 80 | if (syntaxCheck.state() != QScriptSyntaxCheckResult::Valid) { |
| 69 | 81 | auto err = globalObject().property("SyntaxError") |
| @@ -82,13 +94,16 @@ QScriptValue BaseScriptEngine::lintScript(const QString& sourceCode, const QStri · 12 unmodified lines | ||
| 82 | 94 | } |
| 83 | 95 | return err; |
| 84 | 96 | } |
| 85 | − return undefinedValue(); | |
| 97 | + return QScriptValue(); | |
| 86 | 98 | } |
| 87 | 99 | |
| 88 | 100 | // this pulls from the best available information to create a detailed snapshot of the current exception |
| 89 | 101 | QScriptValue BaseScriptEngine::cloneUncaughtException(const QString& extraDetail) { |
| 102 | + if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { | |
| 103 | + return unboundNullValue(); | |
| 104 | + } | |
| 90 | 105 | if (!hasUncaughtException()) { |
| 91 | − return QScriptValue(); | |
| 106 | + return unboundNullValue(); | |
| 92 | 107 | } |
| 93 | 108 | auto exception = uncaughtException(); |
| 94 | 109 | // ensure the error object is engine-local |
| @@ -144,7 +159,10 @@ QScriptValue BaseScriptEngine::cloneUncaughtException(const QString& extraDetail · 49 unmodified lines | ||
| 144 | 159 | return err; |
| 145 | 160 | } |
| 146 | 161 | |
| 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 | + } | |
| 148 | 166 | QString note { "UncaughtException" }; |
| 149 | 167 | QString result; |
| 150 | 168 | |
| @@ -156,8 +174,8 @@ QString BaseScriptEngine::formatException(const QScriptValue& exception) { · 5 unmodified lines | ||
| 156 | 174 | const auto lineNumber = exception.property("lineNumber").toString(); |
| 157 | 175 | const auto stacktrace = exception.property("stack").toString(); |
| 158 | 176 | |
| 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 | |
| 161 | 179 | // Example difference: |
| 162 | 180 | // [UncaughtExceptions] Error: Can't find variable: foobar in atp:/myentity.js\n... |
| 163 | 181 | // [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 | ||
| 173 | 191 | return result; |
| 174 | 192 | } |
| 175 | 193 | |
| 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 | + | |
| 176 | 222 | QScriptValue BaseScriptEngine::evaluateInClosure(const QScriptValue& closure, const QScriptProgram& program) { |
| 177 | 223 | 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(); | |
| 182 | 226 | } |
| 183 | − | |
| 184 | 227 | const auto fileName = program.fileName(); |
| 185 | 228 | const auto shortName = QUrl(fileName).fileName(); |
| 186 | 229 | |
| @@ -189,7 +232,7 @@ QScriptValue BaseScriptEngine::evaluateInClosure(const QScriptValue& closure, co · 2 unmodified lines | ||
| 189 | 232 | auto global = closure.property("global"); |
| 190 | 233 | if (global.isObject()) { |
| 191 | 234 | #ifdef DEBUG_JS |
| 192 | − qCDebug(scriptengine) << " setting global = closure.global" << shortName; | |
| 235 | + qCDebug(shared) << " setting global = closure.global" << shortName; | |
| 193 | 236 | #endif |
| 194 | 237 | oldGlobal = globalObject(); |
| 195 | 238 | setGlobalObject(global); |
| @@ -200,34 +243,34 @@ QScriptValue BaseScriptEngine::evaluateInClosure(const QScriptValue& closure, co · 4 unmodified lines | ||
| 200 | 243 | auto thiz = closure.property("this"); |
| 201 | 244 | if (thiz.isObject()) { |
| 202 | 245 | #ifdef DEBUG_JS |
| 203 | − qCDebug(scriptengine) << " setting this = closure.this" << shortName; | |
| 246 | + qCDebug(shared) << " setting this = closure.this" << shortName; | |
| 204 | 247 | #endif |
| 205 | 248 | context->setThisObject(thiz); |
| 206 | 249 | } |
| 207 | 250 | |
| 208 | 251 | context->pushScope(closure); |
| 209 | 252 | #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); | |
| 211 | 254 | #endif |
| 212 | 255 | { |
| 213 | 256 | result = BaseScriptEngine::evaluate(program); |
| 214 | 257 | if (hasUncaughtException()) { |
| 215 | 258 | auto err = cloneUncaughtException(__FUNCTION__); |
| 216 | 259 | #ifdef DEBUG_JS_EXCEPTIONS |
| 217 | − qCWarning(scriptengine) << __FUNCTION__ << "---------- hasCaught:" << err.toString() << result.toString(); | |
| 260 | + qCWarning(shared) << __FUNCTION__ << "---------- hasCaught:" << err.toString() << result.toString(); | |
| 218 | 261 | err.setProperty("_result", result); |
| 219 | 262 | #endif |
| 220 | 263 | result = err; |
| 221 | 264 | } |
| 222 | 265 | } |
| 223 | 266 | #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); | |
| 225 | 268 | #endif |
| 226 | 269 | popContext(); |
| 227 | 270 | |
| 228 | 271 | if (oldGlobal.isValid()) { |
| 229 | 272 | #ifdef DEBUG_JS |
| 230 | − qCDebug(scriptengine) << " restoring global" << shortName; | |
| 273 | + qCDebug(shared) << " restoring global" << shortName; | |
| 231 | 274 | #endif |
| 232 | 275 | setGlobalObject(oldGlobal); |
| 233 | 276 | } |
| @@ -236,7 +279,6 @@ QScriptValue BaseScriptEngine::evaluateInClosure(const QScriptValue& closure, co · 2 unmodified lines | ||
| 236 | 279 | } |
| 237 | 280 | |
| 238 | 281 | // Lambda |
| 239 | − | |
| 240 | 282 | QScriptValue BaseScriptEngine::newLambdaFunction(std::function<QScriptValue(QScriptContext *, QScriptEngine*)> operation, const QScriptValue& data, const QScriptEngine::ValueOwnership& ownership) { |
| 241 | 283 | auto lambda = new Lambda(this, operation, data); |
| 242 | 284 | auto object = newQObject(lambda, ownership); |
| @@ -262,26 +304,57 @@ Lambda::Lambda(QScriptEngine *engine, std::function<QScriptValue(QScriptContext · 19 unmodified lines | ||
| 262 | 304 | #endif |
| 263 | 305 | } |
| 264 | 306 | QScriptValue Lambda::call() { |
| 307 | + if (!BaseScriptEngine::IS_THREADSAFE_INVOCATION(engine->thread(), __FUNCTION__)) { | |
| 308 | + return BaseScriptEngine::unboundNullValue(); | |
| 309 | + } | |
| 265 | 310 | return operation(engine->currentContext(), engine); |
| 266 | 311 | } |
| 267 | 312 | |
| 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 | + | |
| 268 | 339 | #ifdef DEBUG_JS |
| 269 | 340 | void BaseScriptEngine::_debugDump(const QString& header, const QScriptValue& object, const QString& footer) { |
| 341 | + if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { | |
| 342 | + return; | |
| 343 | + } | |
| 270 | 344 | if (!header.isEmpty()) { |
| 271 | − qCDebug(scriptengine) << header; | |
| 345 | + qCDebug(shared) << header; | |
| 272 | 346 | } |
| 273 | 347 | if (!object.isObject()) { |
| 274 | − qCDebug(scriptengine) << "(!isObject)" << object.toVariant().toString() << object.toString(); | |
| 348 | + qCDebug(shared) << "(!isObject)" << object.toVariant().toString() << object.toString(); | |
| 275 | 349 | return; |
| 276 | 350 | } |
| 277 | 351 | QScriptValueIterator it(object); |
| 278 | 352 | while (it.hasNext()) { |
| 279 | 353 | it.next(); |
| 280 | − qCDebug(scriptengine) << it.name() << ":" << it.value().toString(); | |
| 354 | + qCDebug(shared) << it.name() << ":" << it.value().toString(); | |
| 281 | 355 | } |
| 282 | 356 | if (!footer.isEmpty()) { |
| 283 | − qCDebug(scriptengine) << footer; | |
| 357 | + qCDebug(shared) << footer; | |
| 284 | 358 | } |
| 285 | 359 | } |
| 286 | 360 | #endif |
| 287 | − | |