367 lines · 13.7 KB · C++
RawDownload
////  BaseScriptEngine.cpp//  libraries/script-engine/src////  Created by Timothy Dedischew on 02/01/17.//  Copyright 2017 High Fidelity, Inc.////  Distributed under the Apache License, Version 2.0.//  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html// #include "BaseScriptEngine.h"#include "SharedLogging.h" #include <QtCore/QString>#include <QtCore/QThread>#include <QtCore/QUrl>#include <QtScript/QScriptValue>#include <QtScript/QScriptValueIterator>#include <QtScript/QScriptContextInfo> #include "Profile.h" const QString BaseScriptEngine::SCRIPT_EXCEPTION_FORMAT { "[%0] %1 in %2:%3" };const QString BaseScriptEngine::SCRIPT_BACKTRACE_SEP { "\n    " }; bool BaseScriptEngine::IS_THREADSAFE_INVOCATION(const QThread *thread, const QString& method) {    if (QThread::currentThread() == thread) {        return true;    }    qCCritical(shared) << QString("Scripting::%1 @ %2 -- ignoring thread-unsafe call from %3")        .arg(method).arg(thread ? thread->objectName() : "(!thread)").arg(QThread::currentThread()->objectName());    qCDebug(shared) << "(please resolve on the calling side by using invokeMethod, executeOnScriptThread, etc.)";    Q_ASSERT(false);    return false;} // engine-aware JS Error copier and factoryQScriptValue BaseScriptEngine::makeError(const QScriptValue& _other, const QString& type) {    if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {        return unboundNullValue();    }    auto other = _other;    if (other.isString()) {        other = newObject();        other.setProperty("message", _other.toString());    }    auto proto = globalObject().property(type);    if (!proto.isFunction()) {        proto = globalObject().property(other.prototype().property("constructor").property("name").toString());    }    if (!proto.isFunction()) {#ifdef DEBUG_JS_EXCEPTIONS        qCDebug(shared) << "BaseScriptEngine::makeError -- couldn't find constructor for" << type << " -- using Error instead";#endif        proto = globalObject().property("Error");    }    if (other.engine() != this) {        // JS Objects are parented to a specific script engine instance        // -- this effectively ~clones it locally by routing through a QVariant and back        other = toScriptValue(other.toVariant());    }    // ~ var err = new Error(other.message)    auto err = proto.construct(QScriptValueList({other.property("message")}));     // transfer over any existing properties    QScriptValueIterator it(other);    while (it.hasNext()) {        it.next();        err.setProperty(it.name(), it.value());    }    return err;} // check syntax and when there are issues returns an actual "SyntaxError" with the detailsQScriptValue BaseScriptEngine::lintScript(const QString& sourceCode, const QString& fileName, const int lineNumber) {    if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {        return unboundNullValue();    }    const auto syntaxCheck = checkSyntax(sourceCode);    if (syntaxCheck.state() != QScriptSyntaxCheckResult::Valid) {        auto err = globalObject().property("SyntaxError")            .construct(QScriptValueList({syntaxCheck.errorMessage()}));        err.setProperty("fileName", fileName);        err.setProperty("lineNumber", syntaxCheck.errorLineNumber());        err.setProperty("expressionBeginOffset", syntaxCheck.errorColumnNumber());        err.setProperty("stack", currentContext()->backtrace().join(SCRIPT_BACKTRACE_SEP));        {            const auto error = syntaxCheck.errorMessage();            const auto line = QString::number(syntaxCheck.errorLineNumber());            const auto column = QString::number(syntaxCheck.errorColumnNumber());            // for compatibility with legacy reporting            const auto message = QString("[SyntaxError] %1 in %2:%3(%4)").arg(error, fileName, line, column);            err.setProperty("formatted", message);        }        return err;    }    return QScriptValue();} // this pulls from the best available information to create a detailed snapshot of the current exceptionQScriptValue BaseScriptEngine::cloneUncaughtException(const QString& extraDetail) {    if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {        return unboundNullValue();    }    if (!hasUncaughtException()) {        return unboundNullValue();    }    auto exception = uncaughtException();    // ensure the error object is engine-local    auto err = makeError(exception);     // not sure why Qt does't offer uncaughtExceptionFileName -- but the line number    // on its own is often useless/wrong if arbitrarily married to a filename.    // when the error object already has this info, it seems to be the most reliable    auto fileName = exception.property("fileName").toString();    auto lineNumber = exception.property("lineNumber").toInt32();     // the backtrace, on the other hand, seems most reliable taken from uncaughtExceptionBacktrace    auto backtrace = uncaughtExceptionBacktrace();    if (backtrace.isEmpty()) {        // fallback to the error object        backtrace = exception.property("stack").toString().split(SCRIPT_BACKTRACE_SEP);    }    // the ad hoc "detail" property can be used now to embed additional clues    auto detail = exception.property("detail").toString();    if (detail.isEmpty()) {        detail = extraDetail;    } else if (!extraDetail.isEmpty()) {        detail += "(" + extraDetail + ")";    }    if (lineNumber <= 0) {        lineNumber = uncaughtExceptionLineNumber();    }    if (fileName.isEmpty()) {        // climb the stack frames looking for something useful to display        for (auto c = currentContext(); c && fileName.isEmpty(); c = c->parentContext()) {            QScriptContextInfo info { c };            if (!info.fileName().isEmpty()) {                // take fileName:lineNumber as a pair                fileName = info.fileName();                lineNumber = info.lineNumber();                if (backtrace.isEmpty()) {                    backtrace = c->backtrace();                }                break;            }        }    }    err.setProperty("fileName", fileName);    err.setProperty("lineNumber", lineNumber );    err.setProperty("detail", detail);    err.setProperty("stack", backtrace.join(SCRIPT_BACKTRACE_SEP)); #ifdef DEBUG_JS_EXCEPTIONS    err.setProperty("_fileName", exception.property("fileName").toString());    err.setProperty("_stack", uncaughtExceptionBacktrace().join(SCRIPT_BACKTRACE_SEP));    err.setProperty("_lineNumber", uncaughtExceptionLineNumber());#endif    return err;} QString BaseScriptEngine::formatException(const QScriptValue& exception, bool includeExtendedDetails) {    if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {        return QString();    }    QString note { "UncaughtException" };    QString result;     if (!exception.isObject()) {        return result;    }    const auto message = exception.toString();    const auto fileName = exception.property("fileName").toString();    const auto lineNumber = exception.property("lineNumber").toString();    const auto stacktrace =  exception.property("stack").toString();     if (includeExtendedDetails) {        // Display additional exception / troubleshooting hints that can be added via the custom Error .detail property        // Example difference:        //   [UncaughtExceptions] Error: Can't find variable: foobar in atp:/myentity.js\n...        //   [UncaughtException (construct {1eb5d3fa-23b1-411c-af83-163af7220e3f})] Error: Can't find variable: foobar in atp:/myentity.js\n...        if (exception.property("detail").isValid()) {            note += " " + exception.property("detail").toString();        }    }     result = QString(SCRIPT_EXCEPTION_FORMAT).arg(note, message, fileName, lineNumber);    if (!stacktrace.isEmpty()) {        result += QString("\n[Backtrace]%1%2").arg(SCRIPT_BACKTRACE_SEP).arg(stacktrace);    }    return result;} bool BaseScriptEngine::raiseException(const QScriptValue& exception) {    if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {        return false;    }    if (currentContext()) {        // we have an active context / JS stack frame so throw the exception per usual        currentContext()->throwValue(makeError(exception));        return true;    } else {        // we are within a pure C++ stack frame (ie: being called directly by other C++ code)        // in this case no context information is available so just emit the exception for reporting        emit unhandledException(makeError(exception));    }    return false;} bool BaseScriptEngine::maybeEmitUncaughtException(const QString& debugHint) {    if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {        return false;    }    if (!isEvaluating() && hasUncaughtException()) {        emit unhandledException(cloneUncaughtException(debugHint));        clearExceptions();        return true;    }    return false;} QScriptValue BaseScriptEngine::evaluateInClosure(const QScriptValue& closure, const QScriptProgram& program) {    PROFILE_RANGE(script, "evaluateInClosure");    if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {        return unboundNullValue();    }    const auto fileName = program.fileName();    const auto shortName = QUrl(fileName).fileName();     QScriptValue result;    QScriptValue oldGlobal;    auto global = closure.property("global");    if (global.isObject()) {#ifdef DEBUG_JS        qCDebug(shared) << " setting global = closure.global" << shortName;#endif        oldGlobal = globalObject();        setGlobalObject(global);    }     auto context = pushContext();     auto thiz = closure.property("this");    if (thiz.isObject()) {#ifdef DEBUG_JS        qCDebug(shared) << " setting this = closure.this" << shortName;#endif        context->setThisObject(thiz);    }     context->pushScope(closure);#ifdef DEBUG_JS    qCDebug(shared) << QString("[%1] evaluateInClosure %2").arg(isEvaluating()).arg(shortName);#endif    {        result = BaseScriptEngine::evaluate(program);        if (hasUncaughtException()) {            auto err = cloneUncaughtException(__FUNCTION__);#ifdef DEBUG_JS_EXCEPTIONS            qCWarning(shared) << __FUNCTION__ << "---------- hasCaught:" << err.toString() << result.toString();            err.setProperty("_result", result);#endif            result = err;        }    }#ifdef DEBUG_JS    qCDebug(shared) << QString("[%1] //evaluateInClosure %2").arg(isEvaluating()).arg(shortName);#endif    popContext();     if (oldGlobal.isValid()) {#ifdef DEBUG_JS        qCDebug(shared) << " restoring global" << shortName;#endif        setGlobalObject(oldGlobal);    }     return result;} // LambdaQScriptValue BaseScriptEngine::newLambdaFunction(std::function<QScriptValue(QScriptContext *, QScriptEngine*)> operation, const QScriptValue& data, const QScriptEngine::ValueOwnership& ownership) {    auto lambda = new Lambda(this, operation, data);    auto object = newQObject(lambda, ownership);    auto call = object.property("call");    call.setPrototype(object); // context->callee().prototype() === Lambda QObject    call.setData(data);        // context->callee().data() will === data param    return call;}QString Lambda::toString() const {    return QString("[Lambda%1]").arg(data.isValid() ? " " + data.toString() : data.toString());} Lambda::~Lambda() {#ifdef DEBUG_JS_LAMBDA_FUNCS    qDebug() << "~Lambda" << "this" << this;#endif} Lambda::Lambda(QScriptEngine *engine, std::function<QScriptValue(QScriptContext *, QScriptEngine*)> operation, QScriptValue data)    : engine(engine), operation(operation), data(data) {#ifdef DEBUG_JS_LAMBDA_FUNCS    qDebug() << "Lambda" << data.toString();#endif}QScriptValue Lambda::call() {    if (!BaseScriptEngine::IS_THREADSAFE_INVOCATION(engine->thread(), __FUNCTION__)) {        return BaseScriptEngine::unboundNullValue();    }    return operation(engine->currentContext(), engine);} QScriptValue makeScopedHandlerObject(QScriptValue scopeOrCallback, QScriptValue methodOrName) {    auto engine = scopeOrCallback.engine();    if (!engine) {        return scopeOrCallback;    }    auto scope = QScriptValue();    auto callback = scopeOrCallback;    if (scopeOrCallback.isObject()) {        if (methodOrName.isString()) {            scope = scopeOrCallback;            callback = scope.property(methodOrName.toString());        } else if (methodOrName.isFunction()) {            scope = scopeOrCallback;            callback = methodOrName;        } else if (!methodOrName.isValid()) {            // instantiate from an existing scoped handler object            if (scopeOrCallback.property("callback").isFunction()) {                scope = scopeOrCallback.property("scope");                callback = scopeOrCallback.property("callback");            }        }    }    auto handler = engine->newObject();    handler.setProperty("scope", scope);    handler.setProperty("callback", callback);    return handler;} QScriptValue callScopedHandlerObject(QScriptValue handler, QScriptValue err, QScriptValue result) {    return handler.property("callback").call(handler.property("scope"), QScriptValueList({ err, result }));} #ifdef DEBUG_JSvoid BaseScriptEngine::_debugDump(const QString& header, const QScriptValue& object, const QString& footer) {    if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {        return;    }    if (!header.isEmpty()) {        qCDebug(shared) << header;    }    if (!object.isObject()) {        qCDebug(shared) << "(!isObject)" << object.toVariant().toString() << object.toString();        return;    }    QScriptValueIterator it(object);    while (it.hasNext()) {        it.next();        qCDebug(shared) << it.name() << ":" << it.value().toString();    }    if (!footer.isEmpty()) {        qCDebug(shared) << footer;    }}#endif