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 Load more
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 chain ScriptValue 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 properties ScriptValue 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 BatchLoader QVariantMap ScriptManager::fetchModuleSource(const QString& modulePath, const bool forceDownload) {