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 | + | |