Showing the changes to libraries/script-engine/src/ScriptEngine.cpp (1 of 135 files). Show all
Unified · Split
| @@ -43,6 +43,7 @@ · 42 unmodified lines | ||
| 43 | 43 | |
| 44 | 44 | #include "MIDIEvent.h" |
| 45 | 45 | |
| 46 | + | |
| 46 | 47 | EntityScriptingInterface ScriptEngine::_entityScriptingInterface; |
| 47 | 48 | |
| 48 | 49 | static QScriptValue debugPrint(QScriptContext* context, QScriptEngine* engine){ |
| @@ -96,8 +97,109 @@ ScriptEngine::ScriptEngine(const QString& scriptContents, const QString& fileNam · 47 unmodified lines | ||
| 96 | 97 | _isUserLoaded(false), |
| 97 | 98 | _arrayBufferClass(new ArrayBufferClass(this)) |
| 98 | 99 | { |
| 100 | + _allScriptsMutex.lock(); | |
| 101 | + _allKnownScriptEngines.insert(this); | |
| 102 | + _allScriptsMutex.unlock(); | |
| 103 | +} | |
| 104 | + | |
| 105 | +ScriptEngine::~ScriptEngine() { | |
| 106 | + // If we're not already in the middle of stopping all scripts, then we should remove ourselves | |
| 107 | + // from the list of running scripts. We don't do this if we're in the process of stopping all scripts | |
| 108 | + // because that method removes scripts from its list as it iterates them | |
| 109 | + if (!_stoppingAllScripts) { | |
| 110 | + _allScriptsMutex.lock(); | |
| 111 | + _allKnownScriptEngines.remove(this); | |
| 112 | + _allScriptsMutex.unlock(); | |
| 113 | + } | |
| 114 | +} | |
| 115 | + | |
| 116 | +QSet<ScriptEngine*> ScriptEngine::_allKnownScriptEngines; | |
| 117 | +QMutex ScriptEngine::_allScriptsMutex; | |
| 118 | +bool ScriptEngine::_stoppingAllScripts = false; | |
| 119 | +bool ScriptEngine::_doneRunningThisScript = false; | |
| 120 | + | |
| 121 | +void ScriptEngine::stopAllScripts(QObject* application) { | |
| 122 | + _allScriptsMutex.lock(); | |
| 123 | + _stoppingAllScripts = true; | |
| 124 | + | |
| 125 | + QMutableSetIterator<ScriptEngine*> i(_allKnownScriptEngines); | |
| 126 | + while (i.hasNext()) { | |
| 127 | + ScriptEngine* scriptEngine = i.next(); | |
| 128 | + | |
| 129 | + QString scriptName = scriptEngine->getFilename(); | |
| 130 | + | |
| 131 | + // NOTE: typically all script engines are running. But there's at least one known exception to this, the | |
| 132 | + // "entities sandbox" which is only used to evaluate entities scripts to test their validity before using | |
| 133 | + // them. We don't need to stop scripts that aren't running. | |
| 134 | + if (scriptEngine->isRunning()) { | |
| 135 | + | |
| 136 | + // If the script is running, but still evaluating then we need to wait for its evaluation step to | |
| 137 | + // complete. After that we can handle the stop process appropriately | |
| 138 | + if (scriptEngine->evaluatePending()) { | |
| 139 | + while (scriptEngine->evaluatePending()) { | |
| 140 | + | |
| 141 | + // This event loop allows any started, but not yet finished evaluate() calls to complete | |
| 142 | + // we need to let these complete so that we can be guaranteed that the script engine isn't | |
| 143 | + // in a partially setup state, which can confuse our shutdown unwinding. | |
| 144 | + QEventLoop loop; | |
| 145 | + QObject::connect(scriptEngine, &ScriptEngine::evaluationFinished, &loop, &QEventLoop::quit); | |
| 146 | + loop.exec(); | |
| 147 | + } | |
| 148 | + } | |
| 149 | + | |
| 150 | + // We disconnect any script engine signals from the application because we don't want to do any | |
| 151 | + // extra stopScript/loadScript processing that the Application normally does when scripts start | |
| 152 | + // and stop. We can safely short circuit this because we know we're in the "quitting" process | |
| 153 | + scriptEngine->disconnect(application); | |
| 154 | + | |
| 155 | + // Calling stop on the script engine will set it's internal _isFinished state to true, and result | |
| 156 | + // in the ScriptEngine gracefully ending it's run() method. | |
| 157 | + scriptEngine->stop(); | |
| 158 | + | |
| 159 | + // We need to wait for the engine to be done running before we proceed, because we don't | |
| 160 | + // want any of the scripts final "scriptEnding()" or pending "update()" methods from accessing | |
| 161 | + // any application state after we leave this stopAllScripts() method | |
| 162 | + scriptEngine->waitTillDoneRunning(); | |
| 163 | + | |
| 164 | + // If the script is stopped, we can remove it from our set | |
| 165 | + i.remove(); | |
| 166 | + } | |
| 167 | + } | |
| 168 | + _stoppingAllScripts = false; | |
| 169 | + _allScriptsMutex.unlock(); | |
| 170 | +} | |
| 171 | + | |
| 172 | + | |
| 173 | +void ScriptEngine::waitTillDoneRunning() { | |
| 174 | + QString scriptName = getFilename(); | |
| 175 | + | |
| 176 | + // If the script never started running or finished running before we got here, we don't need to wait for it | |
| 177 | + if (_isRunning) { | |
| 178 | + | |
| 179 | + _doneRunningThisScript = false; // NOTE: this is static, we serialize our waiting for scripts to finish | |
| 180 | + | |
| 181 | + // NOTE: waitTillDoneRunning() will be called on the main Application thread, inside of stopAllScripts() | |
| 182 | + // we want the application thread to continue to process events, because the scripts will likely need to | |
| 183 | + // marshall messages across to the main thread. For example if they access Settings or Meny in any of their | |
| 184 | + // shutdown code. | |
| 185 | + while (!_doneRunningThisScript) { | |
| 186 | + | |
| 187 | + // process events for the main application thread, allowing invokeMethod calls to pass between threads | |
| 188 | + QCoreApplication::processEvents(); | |
| 189 | + } | |
| 190 | + } | |
| 99 | 191 | } |
| 100 | 192 | |
| 193 | +QString ScriptEngine::getFilename() const { | |
| 194 | + QStringList fileNameParts = _fileNameString.split("/"); | |
| 195 | + QString lastPart; | |
| 196 | + if (!fileNameParts.isEmpty()) { | |
| 197 | + lastPart = fileNameParts.last(); | |
| 198 | + } | |
| 199 | + return lastPart; | |
| 200 | +} | |
| 201 | + | |
| 202 | + | |
| 101 | 203 | void ScriptEngine::setIsAvatar(bool isAvatar) { |
| 102 | 204 | _isAvatar = isAvatar; |
| 103 | 205 | |
| @@ -300,12 +402,18 @@ void ScriptEngine::registerGetterSetter(const QString& name, QScriptEngine::Func · 196 unmodified lines | ||
| 300 | 402 | } |
| 301 | 403 | |
| 302 | 404 | void ScriptEngine::evaluate() { |
| 405 | + if (_stoppingAllScripts) { | |
| 406 | + return; // bail early | |
| 407 | + } | |
| 408 | + | |
| 303 | 409 | if (!_isInitialized) { |
| 304 | 410 | init(); |
| 305 | 411 | } |
| 306 | 412 | |
| 307 | 413 | QScriptValue result = evaluate(_scriptContents); |
| 308 | 414 | |
| 415 | + // TODO: why do we check this twice? It seems like the call to clearExcpetions() in the lower level evaluate call | |
| 416 | + // will cause this code to never actually run... | |
| 309 | 417 | if (hasUncaughtException()) { |
| 310 | 418 | int line = uncaughtExceptionLineNumber(); |
| 311 | 419 | qDebug() << "Uncaught exception at (" << _fileNameString << ") line" << line << ":" << result.toString(); |
| @@ -315,11 +423,17 @@ void ScriptEngine::evaluate() { · 3 unmodified lines | ||
| 315 | 423 | } |
| 316 | 424 | |
| 317 | 425 | QScriptValue ScriptEngine::evaluate(const QString& program, const QString& fileName, int lineNumber) { |
| 426 | + if (_stoppingAllScripts) { | |
| 427 | + return QScriptValue(); // bail early | |
| 428 | + } | |
| 429 | + | |
| 430 | + _evaluatesPending++; | |
| 318 | 431 | QScriptValue result = QScriptEngine::evaluate(program, fileName, lineNumber); |
| 319 | 432 | if (hasUncaughtException()) { |
| 320 | 433 | int line = uncaughtExceptionLineNumber(); |
| 321 | 434 | qDebug() << "Uncaught exception at (" << _fileNameString << " : " << fileName << ") line" << line << ": " << result.toString(); |
| 322 | 435 | } |
| 436 | + _evaluatesPending--; | |
| 323 | 437 | emit evaluationFinished(result, hasUncaughtException()); |
| 324 | 438 | clearExceptions(); |
| 325 | 439 | return result; |
| @@ -338,6 +452,9 @@ void ScriptEngine::sendAvatarBillboardPacket() { · 12 unmodified lines | ||
| 338 | 452 | } |
| 339 | 453 | |
| 340 | 454 | void ScriptEngine::run() { |
| 455 | + // TODO: can we add a short circuit for _stoppingAllScripts here? What does it mean to not start running if | |
| 456 | + // we're in the process of stopping? | |
| 457 | + | |
| 341 | 458 | if (!_isInitialized) { |
| 342 | 459 | init(); |
| 343 | 460 | } |
| @@ -346,12 +463,6 @@ void ScriptEngine::run() { · 2 unmodified lines | ||
| 346 | 463 | emit runningStateChanged(); |
| 347 | 464 | |
| 348 | 465 | QScriptValue result = evaluate(_scriptContents); |
| 349 | − if (hasUncaughtException()) { | |
| 350 | − int line = uncaughtExceptionLineNumber(); | |
| 351 | − qDebug() << "Uncaught exception at (" << _fileNameString << ") line" << line << ":" << result.toString(); | |
| 352 | − emit errorMessage("Uncaught exception at (" + _fileNameString + ") line" + QString::number(line) + ":" + result.toString()); | |
| 353 | − clearExceptions(); | |
| 354 | − } | |
| 355 | 466 | |
| 356 | 467 | QElapsedTimer startTime; |
| 357 | 468 | startTime.start(); |
| @@ -378,7 +489,7 @@ void ScriptEngine::run() { · 20 unmodified lines | ||
| 378 | 489 | break; |
| 379 | 490 | } |
| 380 | 491 | |
| 381 | − if (_entityScriptingInterface.getEntityPacketSender()->serversExist()) { | |
| 492 | + if (!_isFinished && _entityScriptingInterface.getEntityPacketSender()->serversExist()) { | |
| 382 | 493 | // release the queue of edit entity messages. |
| 383 | 494 | _entityScriptingInterface.getEntityPacketSender()->releaseQueuedMessages(); |
| 384 | 495 | |
| @@ -388,7 +499,7 @@ void ScriptEngine::run() { · 3 unmodified lines | ||
| 388 | 499 | } |
| 389 | 500 | } |
| 390 | 501 | |
| 391 | − if (_isAvatar && _avatarData) { | |
| 502 | + if (!_isFinished && _isAvatar && _avatarData) { | |
| 392 | 503 | |
| 393 | 504 | const int SCRIPT_AUDIO_BUFFER_SAMPLES = floor(((SCRIPT_DATA_CALLBACK_USECS * AudioConstants::SAMPLE_RATE) |
| 394 | 505 | / (1000 * 1000)) + 0.5); |
| @@ -498,9 +609,14 @@ void ScriptEngine::run() { · 103 unmodified lines | ||
| 498 | 609 | clearExceptions(); |
| 499 | 610 | } |
| 500 | 611 | |
| 501 | − emit update(deltaTime); | |
| 612 | + if (!_isFinished) { | |
| 613 | + emit update(deltaTime); | |
| 614 | + } | |
| 502 | 615 | lastUpdate = now; |
| 616 | + | |
| 503 | 617 | } |
| 618 | + | |
| 619 | + stopAllTimers(); // make sure all our timers are stopped if the script is ending | |
| 504 | 620 | emit scriptEnding(); |
| 505 | 621 | |
| 506 | 622 | // kill the avatar identity timer |
| @@ -525,6 +641,21 @@ void ScriptEngine::run() { · 18 unmodified lines | ||
| 525 | 641 | |
| 526 | 642 | _isRunning = false; |
| 527 | 643 | emit runningStateChanged(); |
| 644 | + | |
| 645 | + emit doneRunning(); | |
| 646 | + | |
| 647 | + _doneRunningThisScript = true; | |
| 648 | +} | |
| 649 | + | |
| 650 | +// NOTE: This is private because it must be called on the same thread that created the timers, which is why | |
| 651 | +// we want to only call it in our own run "shutdown" processing. | |
| 652 | +void ScriptEngine::stopAllTimers() { | |
| 653 | + QMutableHashIterator<QTimer*, QScriptValue> i(_timerFunctionMap); | |
| 654 | + while (i.hasNext()) { | |
| 655 | + i.next(); | |
| 656 | + QTimer* timer = i.key(); | |
| 657 | + stopTimer(timer); | |
| 658 | + } | |
| 528 | 659 | } |
| 529 | 660 | |
| 530 | 661 | void ScriptEngine::stop() { |
| @@ -565,10 +696,20 @@ QObject* ScriptEngine::setupTimerWithInterval(const QScriptValue& function, int · 34 unmodified lines | ||
| 565 | 696 | } |
| 566 | 697 | |
| 567 | 698 | QObject* ScriptEngine::setInterval(const QScriptValue& function, int intervalMS) { |
| 699 | + if (_stoppingAllScripts) { | |
| 700 | + qDebug() << "Script.setInterval() while shutting down is ignored... parent script:" << getFilename(); | |
| 701 | + return NULL; // bail early | |
| 702 | + } | |
| 703 | + | |
| 568 | 704 | return setupTimerWithInterval(function, intervalMS, false); |
| 569 | 705 | } |
| 570 | 706 | |
| 571 | 707 | QObject* ScriptEngine::setTimeout(const QScriptValue& function, int timeoutMS) { |
| 708 | + if (_stoppingAllScripts) { | |
| 709 | + qDebug() << "Script.setTimeout() while shutting down is ignored... parent script:" << getFilename(); | |
| 710 | + return NULL; // bail early | |
| 711 | + } | |
| 712 | + | |
| 572 | 713 | return setupTimerWithInterval(function, timeoutMS, true); |
| 573 | 714 | } |
| 574 | 715 | |
| @@ -609,13 +750,16 @@ void ScriptEngine::print(const QString& message) { · 34 unmodified lines | ||
| 609 | 750 | emit printedMessage(message); |
| 610 | 751 | } |
| 611 | 752 | |
| 612 | −/** | |
| 613 | − * If a callback is specified, the included files will be loaded asynchronously and the callback will be called | |
| 614 | − * when all of the files have finished loading. | |
| 615 | − * If no callback is specified, the included files will be loaded synchronously and will block execution until | |
| 616 | − * all of the files have finished loading. | |
| 617 | − */ | |
| 753 | +// If a callback is specified, the included files will be loaded asynchronously and the callback will be called | |
| 754 | +// when all of the files have finished loading. | |
| 755 | +// If no callback is specified, the included files will be loaded synchronously and will block execution until | |
| 756 | +// all of the files have finished loading. | |
| 618 | 757 | void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callback) { |
| 758 | + if (_stoppingAllScripts) { | |
| 759 | + qDebug() << "Script.include() while shutting down is ignored..." | |
| 760 | + << "includeFiles:" << includeFiles << "parent script:" << getFilename(); | |
| 761 | + return; // bail early | |
| 762 | + } | |
| 619 | 763 | QList<QUrl> urls; |
| 620 | 764 | for (QString file : includeFiles) { |
| 621 | 765 | urls.append(resolvePath(file)); |
| @@ -655,12 +799,27 @@ void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callbac · 33 unmodified lines | ||
| 655 | 799 | } |
| 656 | 800 | |
| 657 | 801 | void ScriptEngine::include(const QString& includeFile, QScriptValue callback) { |
| 802 | + if (_stoppingAllScripts) { | |
| 803 | + qDebug() << "Script.include() while shutting down is ignored... " | |
| 804 | + << "includeFile:" << includeFile << "parent script:" << getFilename(); | |
| 805 | + return; // bail early | |
| 806 | + } | |
| 807 | + | |
| 658 | 808 | QStringList urls; |
| 659 | 809 | urls.append(includeFile); |
| 660 | 810 | include(urls, callback); |
| 661 | 811 | } |
| 662 | 812 | |
| 813 | +// NOTE: The load() command is similar to the include() command except that it loads the script | |
| 814 | +// as a stand-alone script. To accomplish this, the ScriptEngine class just emits a signal which | |
| 815 | +// the Application or other context will connect to in order to know to actually load the script | |
| 663 | 816 | void ScriptEngine::load(const QString& loadFile) { |
| 817 | + if (_stoppingAllScripts) { | |
| 818 | + qDebug() << "Script.load() while shutting down is ignored... " | |
| 819 | + << "loadFile:" << loadFile << "parent script:" << getFilename(); | |
| 820 | + return; // bail early | |
| 821 | + } | |
| 822 | + | |
| 664 | 823 | QUrl url = resolvePath(loadFile); |
| 665 | 824 | emit loadScript(url.toString(), false); |
| 666 | 825 | } |