Showing the changes to libraries/script-engine/src/ScriptEngine.cpp (1 of 194 files). Show all
Unified · Split
| @@ -9,17 +9,26 @@ · 8 unmodified lines | ||
| 9 | 9 | // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html |
| 10 | 10 | // |
| 11 | 11 | |
| 12 | +#include <chrono> | |
| 13 | +#include <thread> | |
| 14 | + | |
| 12 | 15 | #include <QtCore/QCoreApplication> |
| 13 | 16 | #include <QtCore/QEventLoop> |
| 14 | 17 | #include <QtCore/QFileInfo> |
| 15 | 18 | #include <QtCore/QTimer> |
| 16 | 19 | #include <QtCore/QThread> |
| 20 | +#include <QtCore/QRegularExpression> | |
| 21 | + | |
| 22 | +#include <QtWidgets/QMainWindow> | |
| 23 | +#include <QtWidgets/QApplication> | |
| 24 | + | |
| 17 | 25 | #include <QtNetwork/QNetworkRequest> |
| 18 | 26 | #include <QtNetwork/QNetworkReply> |
| 19 | −#include <QtScript/QScriptEngine> | |
| 27 | + | |
| 20 | 28 | #include <QtScript/QScriptValue> |
| 21 | 29 | #include <QtScript/QScriptValueIterator> |
| 22 | −#include <QtCore/QStringList> | |
| 30 | + | |
| 31 | +#include <QtScriptTools/QScriptEngineDebugger> | |
| 23 | 32 | |
| 24 | 33 | #include <AudioConstants.h> |
| 25 | 34 | #include <AudioEffectOptions.h> |
| @@ -31,6 +40,7 @@ · 5 unmodified lines | ||
| 31 | 40 | #include <NodeList.h> |
| 32 | 41 | #include <udt/PacketHeaders.h> |
| 33 | 42 | #include <UUID.h> |
| 43 | +#include <ui/Menu.h> | |
| 34 | 44 | |
| 35 | 45 | #include <controllers/ScriptingInterface.h> |
| 36 | 46 | #include <AnimationObject.h> |
| @@ -164,6 +174,93 @@ void ScriptEngine::disconnectNonEssentialSignals() { · 127 unmodified lines | ||
| 164 | 174 | } |
| 165 | 175 | } |
| 166 | 176 | |
| 177 | +void ScriptEngine::runDebuggable() { | |
| 178 | + static QMenuBar* menuBar { nullptr }; | |
| 179 | + static QMenu* scriptDebugMenu { nullptr }; | |
| 180 | + static size_t scriptMenuCount { 0 }; | |
| 181 | + if (!scriptDebugMenu) { | |
| 182 | + for (auto window : qApp->topLevelWidgets()) { | |
| 183 | + auto mainWindow = qobject_cast<QMainWindow*>(window); | |
| 184 | + if (mainWindow) { | |
| 185 | + menuBar = mainWindow->menuBar(); | |
| 186 | + break; | |
| 187 | + } | |
| 188 | + } | |
| 189 | + if (menuBar) { | |
| 190 | + scriptDebugMenu = menuBar->addMenu("Script Debug"); | |
| 191 | + } | |
| 192 | + } | |
| 193 | + | |
| 194 | + init(); | |
| 195 | + _isRunning = true; | |
| 196 | + _debuggable = true; | |
| 197 | + _debugger = new QScriptEngineDebugger(this); | |
| 198 | + _debugger->attachTo(this); | |
| 199 | + | |
| 200 | + QMenu* parentMenu = scriptDebugMenu; | |
| 201 | + QMenu* scriptMenu { nullptr }; | |
| 202 | + if (parentMenu) { | |
| 203 | + ++scriptMenuCount; | |
| 204 | + scriptMenu = parentMenu->addMenu(_fileNameString); | |
| 205 | + scriptMenu->addMenu(_debugger->createStandardMenu(qApp->activeWindow())); | |
| 206 | + } else { | |
| 207 | + qWarning() << "Unable to add script debug menu"; | |
| 208 | + } | |
| 209 | + | |
| 210 | + QScriptValue result = evaluate(_scriptContents, _fileNameString); | |
| 211 | + | |
| 212 | + _lastUpdate = usecTimestampNow(); | |
| 213 | + QTimer* timer = new QTimer(this); | |
| 214 | + connect(this, &ScriptEngine::finished, [this, timer, parentMenu, scriptMenu] { | |
| 215 | + if (scriptMenu) { | |
| 216 | + parentMenu->removeAction(scriptMenu->menuAction()); | |
| 217 | + --scriptMenuCount; | |
| 218 | + if (0 == scriptMenuCount) { | |
| 219 | + menuBar->removeAction(scriptDebugMenu->menuAction()); | |
| 220 | + scriptDebugMenu = nullptr; | |
| 221 | + } | |
| 222 | + } | |
| 223 | + disconnect(timer); | |
| 224 | + }); | |
| 225 | + | |
| 226 | + connect(timer, &QTimer::timeout, [this, timer] { | |
| 227 | + if (_isFinished) { | |
| 228 | + if (!_isRunning) { | |
| 229 | + return; | |
| 230 | + } | |
| 231 | + stopAllTimers(); // make sure all our timers are stopped if the script is ending | |
| 232 | + if (_wantSignals) { | |
| 233 | + emit scriptEnding(); | |
| 234 | + emit finished(_fileNameString, this); | |
| 235 | + } | |
| 236 | + _isRunning = false; | |
| 237 | + if (_wantSignals) { | |
| 238 | + emit runningStateChanged(); | |
| 239 | + emit doneRunning(); | |
| 240 | + } | |
| 241 | + timer->deleteLater(); | |
| 242 | + return; | |
| 243 | + } | |
| 244 | + | |
| 245 | + qint64 now = usecTimestampNow(); | |
| 246 | + // we check for 'now' in the past in case people set their clock back | |
| 247 | + if (_lastUpdate < now) { | |
| 248 | + float deltaTime = (float)(now - _lastUpdate) / (float)USECS_PER_SECOND; | |
| 249 | + if (!_isFinished) { | |
| 250 | + if (_wantSignals) { | |
| 251 | + emit update(deltaTime); | |
| 252 | + } | |
| 253 | + } | |
| 254 | + } | |
| 255 | + _lastUpdate = now; | |
| 256 | + // Debug and clear exceptions | |
| 257 | + hadUncaughtExceptions(*this, _fileNameString); | |
| 258 | + }); | |
| 259 | + | |
| 260 | + timer->start(10); | |
| 261 | +} | |
| 262 | + | |
| 263 | + | |
| 167 | 264 | void ScriptEngine::runInThread() { |
| 168 | 265 | Q_ASSERT_X(!_isThreaded, "ScriptEngine::runInThread()", "runInThread should not be called more than once"); |
| 169 | 266 | |
| @@ -255,6 +352,10 @@ void ScriptEngine::loadURL(const QUrl& scriptURL, bool reload) { · 85 unmodified lines | ||
| 255 | 352 | // FIXME - switch this to the new model of ScriptCache callbacks |
| 256 | 353 | void ScriptEngine::scriptContentsAvailable(const QUrl& url, const QString& scriptContents) { |
| 257 | 354 | _scriptContents = scriptContents; |
| 355 | + static const QString DEBUG_FLAG("#debug"); | |
| 356 | + if (QRegularExpression(DEBUG_FLAG).match(scriptContents).hasMatch()) { | |
| 357 | + _debuggable = true; | |
| 358 | + } | |
| 258 | 359 | if (_wantSignals) { |
| 259 | 360 | emit scriptLoaded(url.toString()); |
| 260 | 361 | } |
| @@ -704,22 +805,45 @@ void ScriptEngine::run() { · 443 unmodified lines | ||
| 704 | 805 | |
| 705 | 806 | QScriptValue result = evaluate(_scriptContents, _fileNameString); |
| 706 | 807 | |
| 707 | − QElapsedTimer startTime; | |
| 708 | − startTime.start(); | |
| 808 | +#ifdef _WIN32 | |
| 809 | + // VS13 does not sleep_until unless it uses the system_clock, see: | |
| 810 | + // https://www.reddit.com/r/cpp_questions/comments/3o71ic/sleep_until_not_working_with_a_time_pointsteady/ | |
| 811 | + using clock = std::chrono::system_clock; | |
| 812 | +#else | |
| 813 | + using clock = std::chrono::high_resolution_clock; | |
| 814 | +#endif | |
| 709 | 815 | |
| 816 | + clock::time_point startTime = clock::now(); | |
| 710 | 817 | int thisFrame = 0; |
| 711 | 818 | |
| 712 | 819 | auto nodeList = DependencyManager::get<NodeList>(); |
| 713 | 820 | auto entityScriptingInterface = DependencyManager::get<EntityScriptingInterface>(); |
| 714 | 821 | |
| 715 | − qint64 lastUpdate = usecTimestampNow(); | |
| 822 | + _lastUpdate = usecTimestampNow(); | |
| 716 | 823 | |
| 824 | + // TODO: Integrate this with signals/slots instead of reimplementing throttling for ScriptEngine | |
| 717 | 825 | while (!_isFinished) { |
| 718 | − int usecToSleep = (thisFrame++ * SCRIPT_DATA_CALLBACK_USECS) - startTime.nsecsElapsed() / 1000; // nsec to usec | |
| 719 | − if (usecToSleep > 0) { | |
| 720 | − usleep(usecToSleep); | |
| 826 | + // Throttle to SCRIPT_FPS | |
| 827 | + const std::chrono::microseconds FRAME_DURATION(USECS_PER_SECOND / SCRIPT_FPS + 1); | |
| 828 | + clock::time_point sleepTime(startTime + thisFrame++ * FRAME_DURATION); | |
| 829 | + std::this_thread::sleep_until(sleepTime); | |
| 830 | + | |
| 831 | +#ifdef SCRIPT_DELAY_DEBUG | |
| 832 | + { | |
| 833 | + auto now = clock::now(); | |
| 834 | + uint64_t seconds = std::chrono::duration_cast<std::chrono::seconds>(now - startTime).count(); | |
| 835 | + if (seconds > 0) { // avoid division by zero and time travel | |
| 836 | + uint64_t fps = thisFrame / seconds; | |
| 837 | + // Overreporting artificially reduces the reported rate | |
| 838 | + if (thisFrame % SCRIPT_FPS == 0) { | |
| 839 | + qCDebug(scriptengine) << | |
| 840 | + "Frame:" << thisFrame << | |
| 841 | + "Slept (us):" << std::chrono::duration_cast<std::chrono::microseconds>(now - sleepTime).count() << | |
| 842 | + "FPS:" << fps; | |
| 843 | + } | |
| 844 | + } | |
| 721 | 845 | } |
| 722 | − | |
| 846 | +#endif | |
| 723 | 847 | if (_isFinished) { |
| 724 | 848 | break; |
| 725 | 849 | } |
| @@ -743,15 +867,15 @@ void ScriptEngine::run() { · 17 unmodified lines | ||
| 743 | 867 | qint64 now = usecTimestampNow(); |
| 744 | 868 | |
| 745 | 869 | // we check for 'now' in the past in case people set their clock back |
| 746 | − if (lastUpdate < now) { | |
| 747 | − float deltaTime = (float) (now - lastUpdate) / (float) USECS_PER_SECOND; | |
| 870 | + if (_lastUpdate < now) { | |
| 871 | + float deltaTime = (float) (now - _lastUpdate) / (float) USECS_PER_SECOND; | |
| 748 | 872 | if (!_isFinished) { |
| 749 | 873 | if (_wantSignals) { |
| 750 | 874 | emit update(deltaTime); |
| 751 | 875 | } |
| 752 | 876 | } |
| 753 | 877 | } |
| 754 | − lastUpdate = now; | |
| 878 | + _lastUpdate = now; | |
| 755 | 879 | |
| 756 | 880 | // Debug and clear exceptions |
| 757 | 881 | hadUncaughtExceptions(*this, _fileNameString); |
| @@ -967,7 +1091,7 @@ void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callbac · 209 unmodified lines | ||
| 967 | 1091 | } |
| 968 | 1092 | QList<QUrl> urls; |
| 969 | 1093 | bool knowsSensitivity = false; |
| 970 | − Qt::CaseSensitivity sensitivity; | |
| 1094 | + Qt::CaseSensitivity sensitivity { Qt::CaseSensitive }; | |
| 971 | 1095 | auto getSensitivity = [&]() { |
| 972 | 1096 | if (!knowsSensitivity) { |
| 973 | 1097 | QString path = currentSandboxURL.path(); |
| @@ -984,6 +1108,7 @@ void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callbac · 10 unmodified lines | ||
| 984 | 1108 | const auto strippingFlags = QUrl::RemoveFilename | QUrl::RemoveQuery | QUrl::RemoveFragment; |
| 985 | 1109 | for (QString file : includeFiles) { |
| 986 | 1110 | QUrl thisURL; |
| 1111 | + bool isStandardLibrary = false; | |
| 987 | 1112 | if (file.startsWith("/~/")) { |
| 988 | 1113 | thisURL = expandScriptUrl(QUrl::fromLocalFile(expandScriptPath(file))); |
| 989 | 1114 | QUrl defaultScriptsLoc = defaultScriptsLocation(); |
| @@ -991,21 +1116,17 @@ void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callbac · 1 unmodified line | ||
| 991 | 1116 | qDebug() << "ScriptEngine::include -- skipping" << file << "-- outside of standard libraries"; |
| 992 | 1117 | continue; |
| 993 | 1118 | } |
| 1119 | + isStandardLibrary = true; | |
| 994 | 1120 | } else { |
| 995 | 1121 | thisURL = resolvePath(file); |
| 996 | 1122 | } |
| 997 | 1123 | |
| 998 | 1124 | if (!_includedURLs.contains(thisURL)) { |
| 999 | − if (!currentSandboxURL.isEmpty() && (thisURL.scheme() == "file") && | |
| 1000 | − ( | |
| 1001 | − (currentSandboxURL.scheme() != "file") || | |
| 1002 | − ( | |
| 1003 | − !thisURL.toString(strippingFlags).startsWith(defaultScriptsLocation().toString(), getSensitivity()) && | |
| 1004 | − !thisURL.toString(strippingFlags).startsWith(currentSandboxURL.toString(strippingFlags), getSensitivity()) | |
| 1005 | − ) | |
| 1006 | − ) | |
| 1007 | − ) { | |
| 1008 | − qCWarning(scriptengine) << "Script.include() ignoring file path" << thisURL << "outside of original entity script" << currentSandboxURL; | |
| 1125 | + if (!isStandardLibrary && !currentSandboxURL.isEmpty() && (thisURL.scheme() == "file") && | |
| 1126 | + (currentSandboxURL.scheme() != "file" || | |
| 1127 | + !thisURL.toString(strippingFlags).startsWith(currentSandboxURL.toString(strippingFlags), getSensitivity()))) { | |
| 1128 | + qCWarning(scriptengine) << "Script.include() ignoring file path" | |
| 1129 | + << thisURL << "outside of original entity script" << currentSandboxURL; | |
| 1009 | 1130 | } else { |
| 1010 | 1131 | // We could also check here for CORS, but we don't yet. |
| 1011 | 1132 | // It turns out that QUrl.resolve will not change hosts and copy authority, so we don't need to check that here. |