Showing the changes to libraries/script-engine/src/BaseScriptEngine.cpp (1 of 10 files). Show all
Unified · Split
| @@ -0,0 +1,321 @@ | |||
| 1 | // | ||
| 2 | // BaseScriptEngine.cpp | ||
| 3 | // libraries/script-engine/src | ||
| 4 | // | ||
| 5 | // Created by Timothy Dedischew on 02/01/17. | ||
| 6 | // Copyright 2017 High Fidelity, Inc. | ||
| 7 | // | ||
| 8 | // Distributed under the Apache License, Version 2.0. | ||
| 9 | // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html | ||
| 10 | // | ||
| 11 | |||
| 12 | #include "BaseScriptEngine.h" | ||
| 13 | |||
| 14 | #include <QtCore/QString> | ||
| 15 | #include <QtCore/QThread> | ||
| 16 | #include <QtCore/QUrl> | ||
| 17 | #include <QtScript/QScriptValue> | ||
| 18 | #include <QtScript/QScriptValueIterator> | ||
| 19 | #include <QtScript/QScriptContextInfo> | ||
| 20 | |||
| 21 | #include "ScriptEngineLogging.h" | ||
| 22 | #include "Profile.h" | ||
| 23 | |||
| 24 | const QString BaseScriptEngine::_SETTINGS_ENABLE_EXTENDED_EXCEPTIONS { | ||
| 25 | "com.highfidelity.experimental.enableExtendedJSExceptions" | ||
| 26 | }; | ||
| 27 | |||
| 28 | const QString BaseScriptEngine::SCRIPT_EXCEPTION_FORMAT { "[%0] %1 in %2:%3" }; | ||
| 29 | const QString BaseScriptEngine::SCRIPT_BACKTRACE_SEP { "\n " }; | ||
| 30 | |||
| 31 | // engine-aware JS Error copier and factory | ||
| 32 | QScriptValue BaseScriptEngine::makeError(const QScriptValue& _other, const QString& type) { | ||
| 33 | auto other = _other; | ||
| 34 | if (other.isString()) { | ||
| 35 | other = newObject(); | ||
| 36 | other.setProperty("message", _other.toString()); | ||
| 37 | } | ||
| 38 | auto proto = globalObject().property(type); | ||
| 39 | if (!proto.isFunction()) { | ||
| 40 | proto = globalObject().property(other.prototype().property("constructor").property("name").toString()); | ||
| 41 | } | ||
| 42 | if (!proto.isFunction()) { | ||
| 43 | #ifdef DEBUG_JS_EXCEPTIONS | ||
| 44 | qCDebug(scriptengine) << "BaseScriptEngine::makeError -- couldn't find constructor for" << type << " -- using Error instead"; | ||
| 45 | #endif | ||
| 46 | proto = globalObject().property("Error"); | ||
| 47 | } | ||
| 48 | if (other.engine() != this) { | ||
| 49 | // JS Objects are parented to a specific script engine instance | ||
| 50 | // -- this effectively ~clones it locally by routing through a QVariant and back | ||
| 51 | other = toScriptValue(other.toVariant()); | ||
| 52 | } | ||
| 53 | // ~ var err = new Error(other.message) | ||
| 54 | auto err = proto.construct(QScriptValueList({other.property("message")})); | ||
| 55 | |||
| 56 | // transfer over any existing properties | ||
| 57 | QScriptValueIterator it(other); | ||
| 58 | while (it.hasNext()) { | ||
| 59 | it.next(); | ||
| 60 | err.setProperty(it.name(), it.value()); | ||
| 61 | } | ||
| 62 | return err; | ||
| 63 | } | ||
| 64 | |||
| 65 | // check syntax and when there are issues returns an actual "SyntaxError" with the details | ||
| 66 | QScriptValue BaseScriptEngine::lintScript(const QString& sourceCode, const QString& fileName, const int lineNumber) { | ||
| 67 | const auto syntaxCheck = checkSyntax(sourceCode); | ||
| 68 | if (syntaxCheck.state() != QScriptSyntaxCheckResult::Valid) { | ||
| 69 | auto err = globalObject().property("SyntaxError") | ||
| 70 | .construct(QScriptValueList({syntaxCheck.errorMessage()})); | ||
| 71 | err.setProperty("fileName", fileName); | ||
| 72 | err.setProperty("lineNumber", syntaxCheck.errorLineNumber()); | ||
| 73 | err.setProperty("expressionBeginOffset", syntaxCheck.errorColumnNumber()); | ||
| 74 | err.setProperty("stack", currentContext()->backtrace().join(SCRIPT_BACKTRACE_SEP)); | ||
| 75 | { | ||
| 76 | const auto error = syntaxCheck.errorMessage(); | ||
| 77 | const auto line = QString::number(syntaxCheck.errorLineNumber()); | ||
| 78 | const auto column = QString::number(syntaxCheck.errorColumnNumber()); | ||
| 79 | // for compatibility with legacy reporting | ||
| 80 | const auto message = QString("[SyntaxError] %1 in %2:%3(%4)").arg(error, fileName, line, column); | ||
| 81 | err.setProperty("formatted", message); | ||
| 82 | } | ||
| 83 | return err; | ||
| 84 | } | ||
| 85 | return undefinedValue(); | ||
| 86 | } | ||
| 87 | |||
| 88 | // this pulls from the best available information to create a detailed snapshot of the current exception | ||
| 89 | QScriptValue BaseScriptEngine::cloneUncaughtException(const QString& extraDetail) { | ||
| 90 | if (!hasUncaughtException()) { | ||
| 91 | return QScriptValue(); | ||
| 92 | } | ||
| 93 | auto exception = uncaughtException(); | ||
| 94 | // ensure the error object is engine-local | ||
| 95 | auto err = makeError(exception); | ||
| 96 | |||
| 97 | // not sure why Qt does't offer uncaughtExceptionFileName -- but the line number | ||
| 98 | // on its own is often useless/wrong if arbitrarily married to a filename. | ||
| 99 | // when the error object already has this info, it seems to be the most reliable | ||
| 100 | auto fileName = exception.property("fileName").toString(); | ||
| 101 | auto lineNumber = exception.property("lineNumber").toInt32(); | ||
| 102 | |||
| 103 | // the backtrace, on the other hand, seems most reliable taken from uncaughtExceptionBacktrace | ||
| 104 | auto backtrace = uncaughtExceptionBacktrace(); | ||
| 105 | if (backtrace.isEmpty()) { | ||
| 106 | // fallback to the error object | ||
| 107 | backtrace = exception.property("stack").toString().split(SCRIPT_BACKTRACE_SEP); | ||
| 108 | } | ||
| 109 | // the ad hoc "detail" property can be used now to embed additional clues | ||
| 110 | auto detail = exception.property("detail").toString(); | ||
| 111 | if (detail.isEmpty()) { | ||
| 112 | detail = extraDetail; | ||
| 113 | } else if (!extraDetail.isEmpty()) { | ||
| 114 | detail += "(" + extraDetail + ")"; | ||
| 115 | } | ||
| 116 | if (lineNumber <= 0) { | ||
| 117 | lineNumber = uncaughtExceptionLineNumber(); | ||
| 118 | } | ||
| 119 | if (fileName.isEmpty()) { | ||
| 120 | // climb the stack frames looking for something useful to display | ||
| 121 | for(auto ctx = currentContext(); ctx && fileName.isEmpty(); ctx = ctx->parentContext()) { | ||
| 122 | QScriptContextInfo info { ctx }; | ||
| 123 | if (!info.fileName().isEmpty()) { | ||
| 124 | // take fileName:lineNumber as a pair | ||
| 125 | fileName = info.fileName(); | ||
| 126 | lineNumber = info.lineNumber(); | ||
| 127 | if (backtrace.isEmpty()) { | ||
| 128 | backtrace = ctx->backtrace(); | ||
| 129 | } | ||
| 130 | break; | ||
| 131 | } | ||
| 132 | } | ||
| 133 | } | ||
| 134 | err.setProperty("fileName", fileName); | ||
| 135 | err.setProperty("lineNumber", lineNumber ); | ||
| 136 | err.setProperty("detail", detail); | ||
| 137 | err.setProperty("stack", backtrace.join(SCRIPT_BACKTRACE_SEP)); | ||
| 138 | |||
| 139 | #ifdef DEBUG_JS_EXCEPTIONS | ||
| 140 | err.setProperty("_fileName", exception.property("fileName").toString()); | ||
| 141 | err.setProperty("_stack", uncaughtExceptionBacktrace().join(SCRIPT_BACKTRACE_SEP)); | ||
| 142 | err.setProperty("_lineNumber", uncaughtExceptionLineNumber()); | ||
| 143 | #endif | ||
| 144 | return err; | ||
| 145 | } | ||
| 146 | |||
| 147 | QString BaseScriptEngine::formatException(const QScriptValue& exception) { | ||
| 148 | QString note { "UncaughtException" }; | ||
| 149 | QString result; | ||
| 150 | |||
| 151 | if (!exception.isObject()) { | ||
| 152 | return result; | ||
| 153 | } | ||
| 154 | const auto message = exception.toString(); | ||
| 155 | const auto fileName = exception.property("fileName").toString(); | ||
| 156 | const auto lineNumber = exception.property("lineNumber").toString(); | ||
| 157 | const auto stacktrace = exception.property("stack").toString(); | ||
| 158 | |||
| 159 | if (_enableExtendedJSExceptions.get()) { | ||
| 160 | // This setting toggles display of the hints now being added during the loading process. | ||
| 161 | // Example difference: | ||
| 162 | // [UncaughtExceptions] Error: Can't find variable: foobar in atp:/myentity.js\n... | ||
| 163 | // [UncaughtException (construct {1eb5d3fa-23b1-411c-af83-163af7220e3f})] Error: Can't find variable: foobar in atp:/myentity.js\n... | ||
| 164 | if (exception.property("detail").isValid()) { | ||
| 165 | note += " " + exception.property("detail").toString(); | ||
| 166 | } | ||
| 167 | } | ||
| 168 | |||
| 169 | result = QString(SCRIPT_EXCEPTION_FORMAT).arg(note, message, fileName, lineNumber); | ||
| 170 | if (!stacktrace.isEmpty()) { | ||
| 171 | result += QString("\n[Backtrace]%1%2").arg(SCRIPT_BACKTRACE_SEP).arg(stacktrace); | ||
| 172 | } | ||
| 173 | return result; | ||
| 174 | } | ||
| 175 | |||
| 176 | QScriptValue BaseScriptEngine::evaluateInClosure(const QScriptValue& closure, const QScriptProgram& program) { | ||
| 177 | 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(); | ||
| 182 | } | ||
| 183 | |||
| 184 | //_debugDump("evaluateInClosure.closure", closure); | ||
| 185 | |||
| 186 | const auto fileName = program.fileName(); | ||
| 187 | const auto shortName = QUrl(fileName).fileName(); | ||
| 188 | |||
| 189 | QScriptValue result; | ||
| 190 | QScriptValue oldGlobal; | ||
| 191 | auto global = closure.property("global"); | ||
| 192 | if (global.isObject()) { | ||
| 193 | #ifdef DEBUG_JS | ||
| 194 | qCDebug(scriptengine) << " setting global = closure.global" << shortName; | ||
| 195 | #endif | ||
| 196 | oldGlobal = globalObject(); | ||
| 197 | setGlobalObject(global); | ||
| 198 | } | ||
| 199 | |||
| 200 | auto ctx = pushContext(); | ||
| 201 | |||
| 202 | auto thiz = closure.property("this"); | ||
| 203 | if (thiz.isObject()) { | ||
| 204 | #ifdef DEBUG_JS | ||
| 205 | qCDebug(scriptengine) << " setting this = closure.this" << shortName; | ||
| 206 | #endif | ||
| 207 | ctx->setThisObject(thiz); | ||
| 208 | } | ||
| 209 | |||
| 210 | ctx->pushScope(closure); | ||
| 211 | #ifdef DEBUG_JS | ||
| 212 | qCDebug(scriptengine) << QString("[%1] evaluateInClosure %2").arg(isEvaluating()).arg(shortName); | ||
| 213 | #endif | ||
| 214 | { | ||
| 215 | ExceptionEmitter tryCatch(this, __FUNCTION__); | ||
| 216 | result = BaseScriptEngine::evaluate(program); | ||
| 217 | if (tryCatch.hasPending()) { | ||
| 218 | auto err = tryCatch.pending(); | ||
| 219 | #ifdef DEBUG_JS_EXCEPTIONS | ||
| 220 | qCWarning(scriptengine) << __FUNCTION__ << "---------- hasCaught:" << err.toString() << result.toString(); | ||
| 221 | err.setProperty("_result", result); | ||
| 222 | #endif | ||
| 223 | result = err; | ||
| 224 | } | ||
| 225 | } | ||
| 226 | #ifdef DEBUG_JS | ||
| 227 | qCDebug(scriptengine) << QString("[%1] //evaluateInClosure %2").arg(isEvaluating()).arg(shortName); | ||
| 228 | #endif | ||
| 229 | popContext(); | ||
| 230 | |||
| 231 | if (oldGlobal.isValid()) { | ||
| 232 | #ifdef DEBUG_JS | ||
| 233 | qCDebug(scriptengine) << " restoring global" << shortName; | ||
| 234 | #endif | ||
| 235 | setGlobalObject(oldGlobal); | ||
| 236 | } | ||
| 237 | |||
| 238 | return result; | ||
| 239 | } | ||
| 240 | |||
| 241 | // Lambda | ||
| 242 | |||
| 243 | QScriptValue BaseScriptEngine::newLambdaFunction(std::function<QScriptValue(QScriptContext *, QScriptEngine*)> operation, const QScriptValue& data, const QScriptEngine::ValueOwnership& ownership) { | ||
| 244 | auto lambda = new Lambda(this, operation, data); | ||
| 245 | auto object = newQObject(lambda, ownership); | ||
| 246 | auto call = object.property("call"); | ||
| 247 | call.setPrototype(object); // ctx->callee().prototype() === Lambda QObject | ||
| 248 | call.setData(data); // ctx->callee().data() will === data param | ||
| 249 | return call; | ||
| 250 | } | ||
| 251 | QString Lambda::toString() const { | ||
| 252 | return QString("[Lambda%1]").arg(data.isValid() ? " " + data.toString() : data.toString()); | ||
| 253 | } | ||
| 254 | |||
| 255 | Lambda::~Lambda() { | ||
| 256 | #ifdef DEBUG_JS_LAMBDA_FUNCS | ||
| 257 | qDebug() << "~Lambda" << "this" << this; | ||
| 258 | #endif | ||
| 259 | } | ||
| 260 | |||
| 261 | Lambda::Lambda(QScriptEngine *engine, std::function<QScriptValue(QScriptContext *, QScriptEngine*)> operation, QScriptValue data) | ||
| 262 | : engine(engine), operation(operation), data(data) { | ||
| 263 | #ifdef DEBUG_JS_LAMBDA_FUNCS | ||
| 264 | qDebug() << "Lambda" << data.toString(); | ||
| 265 | #endif | ||
| 266 | } | ||
| 267 | QScriptValue Lambda::call() { | ||
| 268 | return operation(engine->currentContext(), engine); | ||
| 269 | } | ||
| 270 | |||
| 271 | // BaseScriptEngine::ExceptionEmitter | ||
| 272 | |||
| 273 | void BaseScriptEngine::_emitUnhandledException(const QScriptValue& exception) { | ||
| 274 | emit unhandledException(exception); | ||
| 275 | } | ||
| 276 | BaseScriptEngine::ExceptionEmitter::ExceptionEmitter(BaseScriptEngine* engine, const QString& debugName) | ||
| 277 | : _engine(engine), _debugName(debugName) { | ||
| 278 | } | ||
| 279 | bool BaseScriptEngine::ExceptionEmitter::hasPending() { | ||
| 280 | return _engine->hasUncaughtException(); | ||
| 281 | } | ||
| 282 | QScriptValue BaseScriptEngine::ExceptionEmitter::pending() { | ||
| 283 | return _engine->cloneUncaughtException(_debugName); | ||
| 284 | } | ||
| 285 | QScriptValue BaseScriptEngine::ExceptionEmitter::consume() { | ||
| 286 | if (hasPending()) { | ||
| 287 | _consumedException = pending(); | ||
| 288 | _engine->clearExceptions(); | ||
| 289 | } | ||
| 290 | return _consumedException; | ||
| 291 | } | ||
| 292 | bool BaseScriptEngine::ExceptionEmitter::wouldEmit() { | ||
| 293 | return !_engine->isEvaluating() && _engine->hasUncaughtException(); | ||
| 294 | } | ||
| 295 | BaseScriptEngine::ExceptionEmitter::~ExceptionEmitter() { | ||
| 296 | if (wouldEmit()) { | ||
| 297 | _engine->_emitUnhandledException(consume()); | ||
| 298 | } | ||
| 299 | } | ||
| 300 | |||
| 301 | |||
| 302 | #ifdef DEBUG_JS | ||
| 303 | void BaseScriptEngine::_debugDump(const QString& header, const QScriptValue& object, const QString& footer) { | ||
| 304 | if (!header.isEmpty()) { | ||
| 305 | qCDebug(scriptengine) << header; | ||
| 306 | } | ||
| 307 | if (!object.isObject()) { | ||
| 308 | qCDebug(scriptengine) << "(!isObject)" << object.toVariant().toString() << object.toString(); | ||
| 309 | return; | ||
| 310 | } | ||
| 311 | QScriptValueIterator it(object); | ||
| 312 | while (it.hasNext()) { | ||
| 313 | it.next(); | ||
| 314 | qCDebug(scriptengine) << it.name() << ":" << it.value().toString(); | ||
| 315 | } | ||
| 316 | if (!footer.isEmpty()) { | ||
| 317 | qCDebug(scriptengine) << footer; | ||
| 318 | } | ||
| 319 | } | ||
| 320 | #endif | ||
| 321 | |||