2763 lines · 116.2 KB · C++
RawDownload
        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, &QTimer::timeout, &loop, &QEventLoop::quit);                timer.start(sleepFor.count());                loop.exec();            } else {                QCoreApplication::processEvents();            }            processedEvents = true;            _engine->perManagerLoopIterationCleanup();        }         PROFILE_RANGE(script, "ScriptMainLoop"); //#define SCRIPT_DELAY_DEBUG#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                const int REPORT_BELOW_FPS = 30;                if (thisFrame % SCRIPT_FPS == 0 && fps < REPORT_BELOW_FPS) {                    qCDebug(scriptengine) <<                        "Script:" << name <<                        "Frame:" << thisFrame <<                        "Slept (us):" << std::chrono::duration_cast<std::chrono::microseconds>(actuallySleptUntil - beforeSleep).count() <<                        "Avg Updates (us):" << averageUpdate.count() <<                        "averageTimerPerFrame (us):" << averageTimerPerFrame.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) {            emit releaseEntityPacketSenderMessages(false);        }         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 (!_engine->isEvaluating() && _engine->hasUncaughtException()) {            qCWarning(scriptengine) << __FUNCTION__ << "---------- UNCAUGHT EXCEPTION --------";            qCWarning(scriptengine) << "runInThread" << _engine->uncaughtException();            emit unhandledException(_engine->uncaughtException());            _engine->clearExceptions();        }    }    scriptInfoMessage("Script Engine stopping:" + getFilename(), getFilename(), -1);     stopAllTimers(); // make sure all our timers are stopped if the script is ending    emit scriptEnding();     emit releaseEntityPacketSenderMessages(true);     emit finished(_fileNameString, shared_from_this());     // Don't leave our local-file-access flag laying around, reset it to false when the scriptengine    // thread is finished    hifi::scripting::setLocalAccessSafeThread(false);    _isRunning = false;    emit runningStateChanged();    emit doneRunning();    _engine->disconnectSignalProxies();    // Process all remaining events    {        QEventLoop loop;        loop.processEvents();    }    _isDoneRunning = true;} // 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 ScriptManager::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 ScriptManager::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 ScriptManager::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();    }} void ScriptManager::updateMemoryCost(const qint64& deltaSize) {    _engine->updateMemoryCost(deltaSize);} void ScriptManager::timerFired() {    if (isStopped()) {        scriptWarningMessage("Script.timerFired() while shutting down is ignored... parent script:" + getFilename(), getFilename(), -1);        return; // bail early    } //#define SCRIPT_TIMER_PERFORMANCE_STATISTICS#ifdef SCRIPT_TIMER_PERFORMANCE_STATISTICS    _timerCallCounter++;    if (_timerCallCounter % 100 == 0) {        qCDebug(scriptengine) << "Script engine: " << _engine->manager()->getFilename()                 << "timer call count: " << _timerCallCounter << " total time: " << _totalTimeInTimerEvents_s;    }    QElapsedTimer callTimer;    callTimer.start();#endif     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, ScriptValueList());        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();    } #ifdef SCRIPT_TIMER_PERFORMANCE_STATISTICS    _totalTimeInTimerEvents_s += callTimer.elapsed() / 1000.0;#endif} QTimer* ScriptManager::setupTimerWithInterval(const ScriptValue& 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, &ScriptManager::timerFired);     // make sure the timer stops when the script does    connect(this, &ScriptManager::scriptEnding, newTimer, &QTimer::stop);      CallbackData timerData = { function, currentEntityIdentifier, currentSandboxURL };    _timerFunctionMap.insert(newTimer, timerData);     newTimer->start(intervalMS);    return newTimer;} QTimer* ScriptManager::setInterval(const ScriptValue& function, int intervalMS) {    if (isStopped()) {        int lineNumber = -1;        QString fileName = getFilename();        auto context = _engine->currentContext();        if (context) {            lineNumber = context->currentLineNumber();            fileName = context->currentFileName();        }        scriptWarningMessage("Script.setInterval() while shutting down is ignored... parent script:" + getFilename(), fileName, lineNumber);        return NULL; // bail early    }     return setupTimerWithInterval(function, intervalMS, false);} QTimer* ScriptManager::setTimeout(const ScriptValue& function, int timeoutMS) {    if (isStopped()) {        int lineNumber = -1;        QString fileName = getFilename();        auto context = _engine->currentContext();        if (context) {            lineNumber = context->currentLineNumber();            fileName = context->currentFileName();        }        scriptWarningMessage("Script.setTimeout() while shutting down is ignored... parent script:" + getFilename(), fileName, lineNumber);        return NULL; // bail early    }     return setupTimerWithInterval(function, timeoutMS, true);} void ScriptManager::stopTimer(QTimer *timer) {    if (_timerFunctionMap.contains(timer)) {        timer->stop();        _timerFunctionMap.remove(timer);        delete timer;    } else {        qCDebug(scriptengine) << "stopTimer -- not in _timerFunctionMap" << timer;    }} QUrl ScriptManager::resolvePath(const QString& include) const {    //qCDebug(scriptengine) << "ScriptManager::resolvePath: getCurrentScriptURLs: " << _engine->getCurrentScriptURLs();    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 = _engine->currentContext();    ScriptContextPointer parentContext;  // using this variable to maintain parent variable lifespan    do {        auto contextInfo = context->functionContext();        parentURL = QUrl(contextInfo->fileName());        //qCDebug(scriptengine) << "ScriptManager::resolvePath: URL get: " << parentURL << " backtrace: " << context->backtrace() << " " << _engine->getCurrentScriptURLs();        parentContext = context->parentContext();        context = parentContext.get();    } 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 ScriptManager::resourcesPath() const {    return QUrl(PathUtils::resourcesUrl());} void ScriptManager::print(const QString& message) {    emit scriptPrintedMessage(message, getFilename(), engine()->currentContext()->currentLineNumber());}  void ScriptManager::beginProfileRange(const QString& label) const {    PROFILE_SYNC_BEGIN(script, label.toStdString().c_str(), label.toStdString().c_str());} void ScriptManager::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 ScriptManager::_requireResolve(const QString& moduleId, const QString& relativeTo) {    if (!_engine->IS_THREADSAFE_INVOCATION(__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 QString& error, const QString &details = QString()) -> QString {        _engine->raiseException(error, "require.resolve: " + details);        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(details, "RangeError");    }     // this regex matches: absolute, dotted or path-like URLs    // (ie: the kind of stuff ScriptManager::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();        QString systemModulePath = QString("%1/modules/%2.js").arg(systemPath).arg(moduleId);        url = defaultScriptsLoc;        url.setPath(systemModulePath);        if (!QFileInfo(url.toLocalFile()).isFile()) {            if (!moduleId.contains("./")) {                // the user might be trying to refer to a relative file without anchoring it                // let's do them a favor and test for that case -- offering specific advice if detected                auto unanchoredUrl = resolvePath("./" + moduleId);                if (QFileInfo(unanchoredUrl.toLocalFile()).isFile()) {                    auto msg = QString("relative module ids must be anchored; use './%1' instead")                        .arg(moduleId);                    return throwResolveError(message.arg(msg));                }            }            return throwResolveError(message.arg("system module not found"));        }    }     if (url.isRelative()) {        return throwResolveError(message.arg("could not resolve module id"));    }     // if it looks like a local file, verify that it's an allowed path and really a file    if (url.isLocalFile()) {        QFileInfo file(url.toLocalFile());        QUrl canonical = url;        if (file.exists()) {            canonical.setPath(file.canonicalFilePath());        }         bool disallowOutsideFiles = !PathUtils::defaultScriptsLocation().isParentOf(canonical) && !currentSandboxURL.isLocalFile();        if (disallowOutsideFiles && !PathUtils::isDescendantOf(canonical, currentSandboxURL)) {            return throwResolveError(message.arg(                QString("path '%1' outside of origin script '%2' '%3'")                    .arg(PathUtils::stripFilename(url))                    .arg(PathUtils::stripFilename(currentSandboxURL))                    .arg(canonical.toString())            ));        }        if (!file.exists()) {            return throwResolveError(message.arg("path does not exist: " + url.toLocalFile()));        }        if (!file.isFile()) {            return throwResolveError(message.arg("path is not a file: " + url.toLocalFile()));        }    }     return url.toString();} // retrieves the current parent module from the JS scope chainScriptValue ScriptManager::currentModule() {    if (!_engine->IS_THREADSAFE_INVOCATION(__FUNCTION__)) {        return _engine->nullValue();    }    auto jsRequire = _engine->globalObject().property("Script").property("require");    auto cache = jsRequire.property("cache");    ScriptValue candidate;    ScriptContextPointer parentContext;  // using this variable to maintain parent variable lifespan    for (auto context = _engine->currentContext(); context && !candidate.isObject();         parentContext = context->parentContext(), context = parentContext.get()) {        auto contextInfo = context->functionContext();        candidate = cache.property(contextInfo->fileName());    }    if (!candidate.isObject()) {        return ScriptValue();    }    return candidate;} // replaces or adds "module" to "parent.children[]" array// (for consistency with Node.js and userscript cache invalidation without "cache busters")bool ScriptManager::registerModuleWithParent(const ScriptValue& module, const ScriptValue& parent) {    auto children = parent.property("children");    if (children.isArray()) {        auto key = module.property("id");        auto length = children.property("length").toInt32();        for (int i = 0; i < length; i++) {            if (children.property(i).property("id").strictlyEquals(key)) {                qCDebug(scriptengine_module) << key.toString() << " updating parent.children[" << i << "] = module";                children.setProperty(i, module);                return true;            }        }        qCDebug(scriptengine_module) << key.toString() << " appending parent.children[" << length << "] = module";        children.setProperty(length, module);        return true;    } else if (parent.isValid()) {        qCDebug(scriptengine_module) << "registerModuleWithParent -- unrecognized parent" << parent.toVariant().toString();    }    return false;} // creates a new JS "module" Object with default metadata propertiesScriptValue ScriptManager::newModule(const QString& modulePath, const ScriptValue& parent) {    auto closure = _engine->newObject();    auto exports = _engine->newObject();    auto module = _engine->newObject();    //qCDebug(scriptengine_module) << "newModule" << parent.property("filename").toString();     closure.setProperty("module", module, READONLY_PROP_FLAGS);     // note: this becomes the "exports" free variable, so should not be set read only    closure.setProperty("exports", exports);     // make the closure available to module instantiation    module.setProperty("__closure__", closure, READONLY_HIDDEN_PROP_FLAGS);     // for consistency with Node.js Module    module.setProperty("id", modulePath, READONLY_PROP_FLAGS);    module.setProperty("filename", modulePath, READONLY_PROP_FLAGS);    module.setProperty("exports", exports); // not readonly    module.setProperty("loaded", false, READONLY_PROP_FLAGS);    module.setProperty("parent", parent, READONLY_PROP_FLAGS);    module.setProperty("children", _engine->newArray(), READONLY_PROP_FLAGS);     // module.require is a bound version of require that always resolves relative to that module's path    auto boundRequire = _engine->evaluate("(function(id) { return Script.require(Script.require.resolve(id, this.filename)); })", "(boundRequire)");    module.setProperty("require", boundRequire, READONLY_PROP_FLAGS);    //qCDebug(scriptengine) << "Module object contents" << _engine->scriptValueDebugListMembers(module);    return module;} // synchronously fetch a module's source code using BatchLoaderQVariantMap ScriptManager::fetchModuleSource(const QString& modulePath, const bool forceDownload) {

Lines 1001–1500 of 2763. Previous lines