2746 lines · 117.8 KB · C++
RawDownload
    if (!_registeredHandlers.contains(entityID)) {        _registeredHandlers[entityID] = RegisteredEventHandlers();    }    CallbackList& handlersForEvent = _registeredHandlers[entityID][eventName];    CallbackData handlerData = { handler, currentEntityIdentifier, currentSandboxURL };    handlersForEvent << handlerData; // Note that the same handler can be added many times. See removeEntityEventHandler().} // this is not redundant -- the version in BaseScriptEngine is specifically not Q_INVOKABLEQScriptValue ScriptEngine::evaluateInClosure(const QScriptValue& closure, const QScriptProgram& program) {    return BaseScriptEngine::evaluateInClosure(closure, program);} QScriptValue ScriptEngine::evaluate(const QString& sourceCode, const QString& fileName, int lineNumber) {    if (DependencyManager::get<ScriptEngines>()->isStopped()) {        return QScriptValue(); // bail early    }     if (QThread::currentThread() != thread()) {        QScriptValue result;#ifdef THREAD_DEBUGGING        qCDebug(scriptengine) << "*** WARNING *** ScriptEngine::evaluate() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] "            "sourceCode:" << sourceCode << " fileName:" << fileName << "lineNumber:" << lineNumber;#endif        BLOCKING_INVOKE_METHOD(this, "evaluate",                                  Q_RETURN_ARG(QScriptValue, result),                                  Q_ARG(const QString&, sourceCode),                                  Q_ARG(const QString&, fileName),                                  Q_ARG(int, lineNumber));        return result;    }     // Check syntax    auto syntaxError = lintScript(sourceCode, fileName);    if (syntaxError.isError()) {        if (!isEvaluating()) {            syntaxError.setProperty("detail", "evaluate");        }        raiseException(syntaxError);        maybeEmitUncaughtException("lint");        return syntaxError;    }    QScriptProgram program { sourceCode, fileName, lineNumber };    if (program.isNull()) {        // can this happen?        auto err = makeError("could not create QScriptProgram for " + fileName);        raiseException(err);        maybeEmitUncaughtException("compile");        return err;    }     QScriptValue result;    {        result = BaseScriptEngine::evaluate(program);        maybeEmitUncaughtException("evaluate");    }    return result;} void ScriptEngine::run() {    auto filenameParts = _fileNameString.split("/");    auto name = filenameParts.size() > 0 ? filenameParts[filenameParts.size() - 1] : "unknown";    PROFILE_SET_THREAD_NAME("Script: " + name);     if (DependencyManager::get<ScriptEngines>()->isStopped()) {        return; // bail early - avoid setting state in init(), as evaluate() will bail too    }     scriptInfoMessage("Script Engine starting:" + getFilename());     if (!_isInitialized) {        init();    }     _isRunning = true;    emit runningStateChanged();     {        PROFILE_RANGE(script, _fileNameString);        evaluate(_scriptContents, _fileNameString);        maybeEmitUncaughtException(__FUNCTION__);    }#ifdef _WIN32    // VS13 does not sleep_until unless it uses the system_clock, see:    // https://www.reddit.com/r/cpp_questions/comments/3o71ic/sleep_until_not_working_with_a_time_pointsteady/    using clock = std::chrono::system_clock;#else    using clock = std::chrono::high_resolution_clock;#endif     clock::time_point startTime = clock::now();    int thisFrame = 0;     auto nodeList = DependencyManager::get<NodeList>();    auto entityScriptingInterface = DependencyManager::get<EntityScriptingInterface>();     _lastUpdate = usecTimestampNow();     std::chrono::microseconds totalUpdates(0);     // TODO: Integrate this with signals/slots instead of reimplementing throttling for ScriptEngine    while (!_isFinished) {        auto beforeSleep = clock::now();         // Throttle to SCRIPT_FPS        // We'd like to try to keep the script at a solid SCRIPT_FPS update rate. And so we will        // calculate a sleepUntil to be the time from our start time until the original target        // sleepUntil for this frame. This approach will allow us to "catch up" in the event        // that some of our script udpates/frames take a little bit longer than the target average        // to execute.        // NOTE: if we go to variable SCRIPT_FPS, then we will need to reconsider this approach        const std::chrono::microseconds TARGET_SCRIPT_FRAME_DURATION(USECS_PER_SECOND / SCRIPT_FPS + 1);        clock::time_point targetSleepUntil(startTime + (thisFrame++ * TARGET_SCRIPT_FRAME_DURATION));         // However, if our sleepUntil is not at least our average update and timer execution time        // into the future it means our script is taking too long in its updates, and we want to        // punish the script a little bit. So we will force the sleepUntil to be at least our        // averageUpdate + averageTimerPerFrame time into the future.        auto averageUpdate = totalUpdates / thisFrame;        auto averageTimerPerFrame = _totalTimerExecution / thisFrame;        auto averageTimerAndUpdate = averageUpdate + averageTimerPerFrame;        auto sleepUntil = std::max(targetSleepUntil, beforeSleep + averageTimerAndUpdate);         // We don't want to actually sleep for too long, because it causes our scripts to hang        // on shutdown and stop... so we want to loop and sleep until we've spent our time in        // purgatory, constantly checking to see if our script was asked to end        bool processedEvents = false;        if (!_isFinished) {            PROFILE_RANGE(script, "processEvents-sleep");            std::chrono::milliseconds sleepFor =                std::chrono::duration_cast<std::chrono::milliseconds>(sleepUntil - clock::now());            if (sleepFor > std::chrono::milliseconds(0)) {                QEventLoop loop;                QTimer timer;                timer.setSingleShot(true);                connect(&timer, SIGNAL(timeout()), &loop, SLOT(quit()));                timer.start(sleepFor.count());                loop.exec();            } else {                QCoreApplication::processEvents();            }            processedEvents = true;        }         PROFILE_RANGE(script, "ScriptMainLoop"); #ifdef SCRIPT_DELAY_DEBUG        {            auto actuallySleptUntil = clock::now();            uint64_t seconds = std::chrono::duration_cast<std::chrono::seconds>(actuallySleptUntil - startTime).count();            if (seconds > 0) { // avoid division by zero and time travel                uint64_t fps = thisFrame / seconds;                // Overreporting artificially reduces the reported rate                if (thisFrame % SCRIPT_FPS == 0) {                    qCDebug(scriptengine) <<                        "Frame:" << thisFrame <<                        "Slept (us):" << std::chrono::duration_cast<std::chrono::microseconds>(actuallySleptUntil - beforeSleep).count() <<                        "Avg Updates (us):" << averageUpdate.count() <<                        "FPS:" << fps;                }            }        }#endif        if (_isFinished) {            break;        }         // Only call this if we didn't processEvents as part of waiting for next frame        if (!processedEvents) {            PROFILE_RANGE(script, "processEvents");            QCoreApplication::processEvents();        }         if (_isFinished) {            break;        }         if (!_isFinished && entityScriptingInterface->getEntityPacketSender()->serversExist()) {            // release the queue of edit entity messages.            entityScriptingInterface->getEntityPacketSender()->releaseQueuedMessages();             // since we're in non-threaded mode, call process so that the packets are sent            if (!entityScriptingInterface->getEntityPacketSender()->isThreaded()) {                entityScriptingInterface->getEntityPacketSender()->process();            }        }         qint64 now = usecTimestampNow();         // we check for 'now' in the past in case people set their clock back        if (_emitScriptUpdates() && _lastUpdate < now) {            float deltaTime = (float) (now - _lastUpdate) / (float) USECS_PER_SECOND;            if (!_isFinished) {                auto preUpdate = clock::now();                {                    PROFILE_RANGE(script, "ScriptUpdate");                    emit update(deltaTime);                }                auto postUpdate = clock::now();                auto elapsed = (postUpdate - preUpdate);                totalUpdates += std::chrono::duration_cast<std::chrono::microseconds>(elapsed);            }        }        _lastUpdate = now;         // only clear exceptions if we are not in the middle of evaluating        if (!isEvaluating() && hasUncaughtException()) {            qCWarning(scriptengine) << __FUNCTION__ << "---------- UNCAUGHT EXCEPTION --------";            qCWarning(scriptengine) << "runInThread" << uncaughtException().toString();            emit unhandledException(cloneUncaughtException(__FUNCTION__));            clearExceptions();        }    }    scriptInfoMessage("Script Engine stopping:" + getFilename());     stopAllTimers(); // make sure all our timers are stopped if the script is ending    emit scriptEnding();     if (entityScriptingInterface->getEntityPacketSender()->serversExist()) {        // release the queue of edit entity messages.        entityScriptingInterface->getEntityPacketSender()->releaseQueuedMessages();         // since we're in non-threaded mode, call process so that the packets are sent        if (!entityScriptingInterface->getEntityPacketSender()->isThreaded()) {            // wait here till the edit packet sender is completely done sending            while (entityScriptingInterface->getEntityPacketSender()->hasPacketsToSend()) {                entityScriptingInterface->getEntityPacketSender()->process();                QCoreApplication::processEvents();            }        } else {            // FIXME - do we need to have a similar "wait here" loop for non-threaded packet senders?        }    }     emit finished(_fileNameString, qSharedPointerCast<ScriptEngine>(sharedFromThis()));     _isRunning = false;    emit runningStateChanged();    emit doneRunning();} // NOTE: This is private because it must be called on the same thread that created the timers, which is why// we want to only call it in our own run "shutdown" processing.void ScriptEngine::stopAllTimers() {    QMutableHashIterator<QTimer*, CallbackData> i(_timerFunctionMap);    int j {0};    while (i.hasNext()) {        i.next();        QTimer* timer = i.key();        qCDebug(scriptengine) << getFilename() << "stopAllTimers[" << j++ << "]";        stopTimer(timer);    }} void ScriptEngine::stopAllTimersForEntityScript(const EntityItemID& entityID) {     // We could maintain a separate map of entityID => QTimer, but someone will have to prove to me that it's worth the complexity. -HRS    QVector<QTimer*> toDelete;    QMutableHashIterator<QTimer*, CallbackData> i(_timerFunctionMap);    while (i.hasNext()) {        i.next();        if (i.value().definingEntityIdentifier != entityID) {            continue;        }        QTimer* timer = i.key();        toDelete << timer; // don't delete while we're iterating. save it.    }    for (auto timer:toDelete) { // now reap 'em        stopTimer(timer);    } } void ScriptEngine::stop(bool marshal) {    _isStopping = true; // this can be done on any thread     if (marshal) {        QMetaObject::invokeMethod(this, "stop");        return;    }    if (!_isFinished) {        _isFinished = true;        emit runningStateChanged();    }} // Other threads can invoke this through invokeMethod, which causes the callback to be asynchronously executed in this script's thread.void ScriptEngine::callAnimationStateHandler(QScriptValue callback, AnimVariantMap parameters, QStringList names, bool useNames, AnimVariantResultHandler resultHandler) {    if (QThread::currentThread() != thread()) {#ifdef THREAD_DEBUGGING        qCDebug(scriptengine) << "*** WARNING *** ScriptEngine::callAnimationStateHandler() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "]  name:" << name;#endif        QMetaObject::invokeMethod(this, "callAnimationStateHandler",                                  Q_ARG(QScriptValue, callback),                                  Q_ARG(AnimVariantMap, parameters),                                  Q_ARG(QStringList, names),                                  Q_ARG(bool, useNames),                                  Q_ARG(AnimVariantResultHandler, resultHandler));        return;    }    QScriptValue javascriptParameters = parameters.animVariantMapToScriptValue(this, names, useNames);    QScriptValueList callingArguments;    callingArguments << javascriptParameters;    assert(currentEntityIdentifier.isInvalidID()); // No animation state handlers from entity scripts.    QScriptValue result = callback.call(QScriptValue(), callingArguments);     // validate result from callback function.    if (result.isValid() && result.isObject()) {        resultHandler(result);    } else {        qCWarning(scriptengine) << "ScriptEngine::callAnimationStateHandler invalid return argument from callback, expected an object";    }} void ScriptEngine::updateMemoryCost(const qint64& deltaSize) {    if (deltaSize > 0) {        reportAdditionalMemoryCost(deltaSize);    }} void ScriptEngine::timerFired() {    {        auto engine = DependencyManager::get<ScriptEngines>();        if (!engine || engine->isStopped()) {            scriptWarningMessage("Script.timerFired() while shutting down is ignored... parent script:" + getFilename());            return; // bail early        }    }     QTimer* callingTimer = reinterpret_cast<QTimer*>(sender());    CallbackData timerData = _timerFunctionMap.value(callingTimer);     if (!callingTimer->isActive()) {        // this timer is done, we can kill it        _timerFunctionMap.remove(callingTimer);        delete callingTimer;    }     // call the associated JS function, if it exists    if (timerData.function.isValid()) {        PROFILE_RANGE(script, __FUNCTION__);        auto preTimer = p_high_resolution_clock::now();        callWithEnvironment(timerData.definingEntityIdentifier, timerData.definingSandboxURL, timerData.function, timerData.function, QScriptValueList());        auto postTimer = p_high_resolution_clock::now();        auto elapsed = (postTimer - preTimer);        _totalTimerExecution += std::chrono::duration_cast<std::chrono::microseconds>(elapsed);    } else {        qCWarning(scriptengine) << "timerFired -- invalid function" << timerData.function.toVariant().toString();    }} QObject* ScriptEngine::setupTimerWithInterval(const QScriptValue& function, int intervalMS, bool isSingleShot) {    // create the timer, add it to the map, and start it    QTimer* newTimer = new QTimer(this);    newTimer->setSingleShot(isSingleShot);     // The default timer type is not very accurate below about 200ms http://doc.qt.io/qt-5/qt.html#TimerType-enum    static const int MIN_TIMEOUT_FOR_COARSE_TIMER = 200;    if (intervalMS < MIN_TIMEOUT_FOR_COARSE_TIMER) {        newTimer->setTimerType(Qt::PreciseTimer);    }     connect(newTimer, &QTimer::timeout, this, &ScriptEngine::timerFired);     // make sure the timer stops when the script does    connect(this, &ScriptEngine::scriptEnding, newTimer, &QTimer::stop);      CallbackData timerData = { function, currentEntityIdentifier, currentSandboxURL };    _timerFunctionMap.insert(newTimer, timerData);     newTimer->start(intervalMS);    return newTimer;} QObject* ScriptEngine::setInterval(const QScriptValue& function, int intervalMS) {    if (DependencyManager::get<ScriptEngines>()->isStopped()) {        scriptWarningMessage("Script.setInterval() while shutting down is ignored... parent script:" + getFilename());        return NULL; // bail early    }     return setupTimerWithInterval(function, intervalMS, false);} QObject* ScriptEngine::setTimeout(const QScriptValue& function, int timeoutMS) {    if (DependencyManager::get<ScriptEngines>()->isStopped()) {        scriptWarningMessage("Script.setTimeout() while shutting down is ignored... parent script:" + getFilename());        return NULL; // bail early    }     return setupTimerWithInterval(function, timeoutMS, true);} void ScriptEngine::stopTimer(QTimer *timer) {    if (_timerFunctionMap.contains(timer)) {        timer->stop();        _timerFunctionMap.remove(timer);        delete timer;    } else {        qCDebug(scriptengine) << "stopTimer -- not in _timerFunctionMap" << timer;    }} QUrl ScriptEngine::resolvePath(const QString& include) const {    QUrl url(include);    // first lets check to see if it's already a full URL -- or a Windows path like "c:/"    if (include.startsWith("/") || url.scheme().length() == 1) {        url = QUrl::fromLocalFile(include);    }    if (!url.isRelative()) {        return expandScriptUrl(url);    }     // we apparently weren't a fully qualified url, so, let's assume we're relative    // to the first absolute URL in the JS scope chain    QUrl parentURL;    auto context = currentContext();    do {        QScriptContextInfo contextInfo { context };        parentURL = QUrl(contextInfo.fileName());        context = context->parentContext();    } while (parentURL.isRelative() && context);     if (parentURL.isRelative()) {        // fallback to the "include" parent (if defined, this will already be absolute)        parentURL = QUrl(_parentURL);    }     if (parentURL.isRelative()) {        // fallback to the original script engine URL        parentURL = QUrl(_fileNameString);         // if still relative and path-like, then this is probably a local file...        if (parentURL.isRelative() && url.path().contains("/")) {            parentURL = QUrl::fromLocalFile(_fileNameString);        }    }     // at this point we should have a legitimate fully qualified URL for our parent    url = expandScriptUrl(parentURL.resolved(url));    return url;} QUrl ScriptEngine::resourcesPath() const {    return QUrl(PathUtils::resourcesUrl());} void ScriptEngine::print(const QString& message) {    emit printedMessage(message, getFilename());}  void ScriptEngine::beginProfileRange(const QString& label) const {    PROFILE_SYNC_BEGIN(script, label.toStdString().c_str(), label.toStdString().c_str());} void ScriptEngine::endProfileRange(const QString& label) const {    PROFILE_SYNC_END(script, label.toStdString().c_str(), label.toStdString().c_str());} // Script.require.resolve -- like resolvePath, but performs more validation and throws exceptions on invalid module identifiers (for consistency with Node.js)QString ScriptEngine::_requireResolve(const QString& moduleId, const QString& relativeTo) {    if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {        return QString();    }    QUrl defaultScriptsLoc = PathUtils::defaultScriptsLocation();    QUrl url(moduleId);     auto displayId = moduleId;    if (displayId.length() > MAX_DEBUG_VALUE_LENGTH) {        displayId = displayId.mid(0, MAX_DEBUG_VALUE_LENGTH) + "...";    }    auto message = QString("Cannot find module '%1' (%2)").arg(displayId);     auto throwResolveError = [&](const QScriptValue& error) -> QString {        raiseException(error);        maybeEmitUncaughtException("require.resolve");        return QString();    };     // de-fuzz the input a little by restricting to rational sizes    auto idLength = url.toString().length();    if (idLength < 1 || idLength > MAX_MODULE_ID_LENGTH) {        auto details = QString("rejecting invalid module id size (%1 chars [1,%2])")            .arg(idLength).arg(MAX_MODULE_ID_LENGTH);        return throwResolveError(makeError(message.arg(details), "RangeError"));    }     // this regex matches: absolute, dotted or path-like URLs    // (ie: the kind of stuff ScriptEngine::resolvePath already handles)    QRegularExpression qualified ("^\\w+:|^/|^[.]{1,2}(/|$)");     // this is for module.require (which is a bound version of require that's always relative to the module path)    if (!relativeTo.isEmpty()) {        url = QUrl(relativeTo).resolved(moduleId);        url = resolvePath(url.toString());    } else if (qualified.match(moduleId).hasMatch()) {        url = resolvePath(moduleId);    } else {        // check if the moduleId refers to a "system" module        QString systemPath = defaultScriptsLoc.path();

Lines 1001–1500 of 2746. Previous lines