Showing the changes to libraries/script-engine/src/ScriptEngine.cpp (1 of 260 files). Show all
Unified · Split
| @@ -1,2844 +0,0 @@ | |||
| 1 | // | ||
| 2 | // ScriptEngine.cpp | ||
| 3 | // libraries/script-engine/src | ||
| 4 | // | ||
| 5 | // Created by Brad Hefta-Gaub on 12/14/13. | ||
| 6 | // Copyright 2013 High Fidelity, Inc. | ||
| 7 | // | ||
| 8 | // Distributed under the Apache License, Version 2.0. | ||
| 9 | // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html | ||
| 10 | // | ||
| 11 | |||
| 12 | #include "ScriptEngine.h" | ||
| 13 | |||
| 14 | #include <chrono> | ||
| 15 | #include <thread> | ||
| 16 | |||
| 17 | #include <QtCore/QCoreApplication> | ||
| 18 | #include <QtCore/QEventLoop> | ||
| 19 | #include <QtCore/QFileInfo> | ||
| 20 | #include <QtCore/QTimer> | ||
| 21 | #include <QtCore/QThread> | ||
| 22 | #include <QtCore/QRegularExpression> | ||
| 23 | |||
| 24 | #include <QtCore/QFuture> | ||
| 25 | #include <QtConcurrent/QtConcurrentRun> | ||
| 26 | |||
| 27 | #include <QtWidgets/QMainWindow> | ||
| 28 | #include <QtWidgets/QApplication> | ||
| 29 | #include <QtWidgets/QMenuBar> | ||
| 30 | #include <QtWidgets/QMenu> | ||
| 31 | |||
| 32 | #include <QtNetwork/QNetworkRequest> | ||
| 33 | #include <QtNetwork/QNetworkReply> | ||
| 34 | |||
| 35 | #include <QtScript/QScriptContextInfo> | ||
| 36 | #include <QtScript/QScriptValue> | ||
| 37 | #include <QtScript/QScriptValueIterator> | ||
| 38 | |||
| 39 | #include <shared/LocalFileAccessGate.h> | ||
| 40 | #include <shared/QtHelpers.h> | ||
| 41 | #include <shared/AbstractLoggerInterface.h> | ||
| 42 | #include <AudioConstants.h> | ||
| 43 | #include <AudioEffectOptions.h> | ||
| 44 | #include <AvatarData.h> | ||
| 45 | #include <DebugDraw.h> | ||
| 46 | #include <EntityScriptingInterface.h> | ||
| 47 | #include <MessagesClient.h> | ||
| 48 | #include <NetworkAccessManager.h> | ||
| 49 | #include <PathUtils.h> | ||
| 50 | #include <ResourceScriptingInterface.h> | ||
| 51 | #include <UserActivityLoggerScriptingInterface.h> | ||
| 52 | #include <NodeList.h> | ||
| 53 | #include <ScriptAvatarData.h> | ||
| 54 | #include <udt/PacketHeaders.h> | ||
| 55 | #include <UUID.h> | ||
| 56 | |||
| 57 | #include <controllers/ScriptingInterface.h> | ||
| 58 | #include <AnimationObject.h> | ||
| 59 | |||
| 60 | #include "ArrayBufferViewClass.h" | ||
| 61 | #include "AssetScriptingInterface.h" | ||
| 62 | #include "BatchLoader.h" | ||
| 63 | #include "BaseScriptEngine.h" | ||
| 64 | #include "DataViewClass.h" | ||
| 65 | #include "EventTypes.h" | ||
| 66 | #include "FileScriptingInterface.h" // unzip project | ||
| 67 | #include "MenuItemProperties.h" | ||
| 68 | #include "ScriptAudioInjector.h" | ||
| 69 | #include "ScriptAvatarData.h" | ||
| 70 | #include "ScriptCache.h" | ||
| 71 | #include "ScriptEngineLogging.h" | ||
| 72 | #include "TypedArrays.h" | ||
| 73 | #include "XMLHttpRequestClass.h" | ||
| 74 | #include "WebSocketClass.h" | ||
| 75 | #include "RecordingScriptingInterface.h" | ||
| 76 | #include "ScriptEngines.h" | ||
| 77 | #include "StackTestScriptingInterface.h" | ||
| 78 | #include "ModelScriptingInterface.h" | ||
| 79 | |||
| 80 | #include <Profile.h> | ||
| 81 | |||
| 82 | #include "../../midi/src/Midi.h" // FIXME why won't a simpler include work? | ||
| 83 | #include "MIDIEvent.h" | ||
| 84 | |||
| 85 | #include "SettingHandle.h" | ||
| 86 | #include <AddressManager.h> | ||
| 87 | #include <NetworkingConstants.h> | ||
| 88 | #include <ThreadHelpers.h> | ||
| 89 | |||
| 90 | const QString ScriptEngine::_SETTINGS_ENABLE_EXTENDED_EXCEPTIONS { | ||
| 91 | "com.highfidelity.experimental.enableExtendedJSExceptions" | ||
| 92 | }; | ||
| 93 | |||
| 94 | static const int MAX_MODULE_ID_LENGTH { 4096 }; | ||
| 95 | static const int MAX_DEBUG_VALUE_LENGTH { 80 }; | ||
| 96 | |||
| 97 | static const QScriptEngine::QObjectWrapOptions DEFAULT_QOBJECT_WRAP_OPTIONS = | ||
| 98 | QScriptEngine::ExcludeDeleteLater | QScriptEngine::ExcludeChildObjects; | ||
| 99 | static const QScriptValue::PropertyFlags READONLY_PROP_FLAGS { QScriptValue::ReadOnly | QScriptValue::Undeletable }; | ||
| 100 | static const QScriptValue::PropertyFlags READONLY_HIDDEN_PROP_FLAGS { READONLY_PROP_FLAGS | QScriptValue::SkipInEnumeration }; | ||
| 101 | |||
| 102 | static const bool HIFI_AUTOREFRESH_FILE_SCRIPTS { true }; | ||
| 103 | |||
| 104 | Q_DECLARE_METATYPE(QScriptEngine::FunctionSignature) | ||
| 105 | int functionSignatureMetaID = qRegisterMetaType<QScriptEngine::FunctionSignature>(); | ||
| 106 | |||
| 107 | int scriptEnginePointerMetaID = qRegisterMetaType<ScriptEnginePointer>(); | ||
| 108 | |||
| 109 | Q_DECLARE_METATYPE(ExternalResource::Bucket); | ||
| 110 | |||
| 111 | static QScriptValue debugPrint(QScriptContext* context, QScriptEngine* engine) { | ||
| 112 | // assemble the message by concatenating our arguments | ||
| 113 | QString message = ""; | ||
| 114 | for (int i = 0; i < context->argumentCount(); i++) { | ||
| 115 | if (i > 0) { | ||
| 116 | message += " "; | ||
| 117 | } | ||
| 118 | message += context->argument(i).toString(); | ||
| 119 | } | ||
| 120 | |||
| 121 | // was this generated by a script engine? If we don't recognize it then send the message and exit | ||
| 122 | ScriptEngine* scriptEngine = qobject_cast<ScriptEngine*>(engine); | ||
| 123 | if (!scriptEngine) { | ||
| 124 | qCDebug(scriptengine_script, "%s", qUtf8Printable(message)); | ||
| 125 | return QScriptValue(); | ||
| 126 | } | ||
| 127 | |||
| 128 | // This message was sent by one of our script engines, let's try to see if we can find the source. | ||
| 129 | // Note that the first entry in the backtrace should be "print" and is somewhat useless to us | ||
| 130 | AbstractLoggerInterface* loggerInterface = AbstractLoggerInterface::get(); | ||
| 131 | if (loggerInterface && loggerInterface->showSourceDebugging()) { | ||
| 132 | QScriptContext* userContext = context; | ||
| 133 | while (userContext && QScriptContextInfo(userContext).functionType() == QScriptContextInfo::NativeFunction) { | ||
| 134 | userContext = userContext->parentContext(); | ||
| 135 | } | ||
| 136 | QString location; | ||
| 137 | if (userContext) { | ||
| 138 | QScriptContextInfo contextInfo(userContext); | ||
| 139 | QString fileName = contextInfo.fileName(); | ||
| 140 | int lineNumber = contextInfo.lineNumber(); | ||
| 141 | QString functionName = contextInfo.functionName(); | ||
| 142 | |||
| 143 | location = functionName; | ||
| 144 | if (!fileName.isEmpty()) { | ||
| 145 | if (location.isEmpty()) { | ||
| 146 | location = fileName; | ||
| 147 | } else { | ||
| 148 | location = QString("%1 at %2").arg(location).arg(fileName); | ||
| 149 | } | ||
| 150 | } | ||
| 151 | if (lineNumber != -1) { | ||
| 152 | location = QString("%1:%2").arg(location).arg(lineNumber); | ||
| 153 | } | ||
| 154 | } | ||
| 155 | if (location.isEmpty()) { | ||
| 156 | location = scriptEngine->getFilename(); | ||
| 157 | } | ||
| 158 | |||
| 159 | // give the script engine a chance to notify the system about this message | ||
| 160 | scriptEngine->print(message); | ||
| 161 | |||
| 162 | // send the message to debug log | ||
| 163 | qCDebug(scriptengine_script, "[%s] %s", qUtf8Printable(location), qUtf8Printable(message)); | ||
| 164 | } else { | ||
| 165 | scriptEngine->print(message); | ||
| 166 | // prefix the script engine name to help disambiguate messages in the main debug log | ||
| 167 | qCDebug(scriptengine_script, "[%s] %s", qUtf8Printable(scriptEngine->getFilename()), qUtf8Printable(message)); | ||
| 168 | } | ||
| 169 | |||
| 170 | return QScriptValue(); | ||
| 171 | } | ||
| 172 | |||
| 173 | Q_DECLARE_METATYPE(controller::InputController*) | ||
| 174 | //static int inputControllerPointerId = qRegisterMetaType<controller::InputController*>(); | ||
| 175 | |||
| 176 | QScriptValue inputControllerToScriptValue(QScriptEngine *engine, controller::InputController* const &in) { | ||
| 177 | return engine->newQObject(in, QScriptEngine::QtOwnership, DEFAULT_QOBJECT_WRAP_OPTIONS); | ||
| 178 | } | ||
| 179 | |||
| 180 | void inputControllerFromScriptValue(const QScriptValue &object, controller::InputController* &out) { | ||
| 181 | out = qobject_cast<controller::InputController*>(object.toQObject()); | ||
| 182 | } | ||
| 183 | |||
| 184 | // FIXME Come up with a way to properly encode entity IDs in filename | ||
| 185 | // The purpose of the following two function is to embed entity ids into entity script filenames | ||
| 186 | // so that they show up in stacktraces | ||
| 187 | // | ||
| 188 | // Extract the url portion of a url that has been encoded with encodeEntityIdIntoEntityUrl(...) | ||
| 189 | QString extractUrlFromEntityUrl(const QString& url) { | ||
| 190 | auto parts = url.split(' ', Qt::SkipEmptyParts); | ||
| 191 | if (parts.length() > 0) { | ||
| 192 | return parts[0]; | ||
| 193 | } else { | ||
| 194 | return ""; | ||
| 195 | } | ||
| 196 | } | ||
| 197 | |||
| 198 | // Encode an entity id into an entity url | ||
| 199 | // Example: http://www.example.com/some/path.js [EntityID:{9fdd355f-d226-4887-9484-44432d29520e}] | ||
| 200 | QString encodeEntityIdIntoEntityUrl(const QString& url, const QString& entityID) { | ||
| 201 | return url + " [EntityID:" + entityID + "]"; | ||
| 202 | } | ||
| 203 | |||
| 204 | QString ScriptEngine::logException(const QScriptValue& exception) { | ||
| 205 | auto message = formatException(exception, _enableExtendedJSExceptions.get()); | ||
| 206 | scriptErrorMessage(message); | ||
| 207 | return message; | ||
| 208 | } | ||
| 209 | |||
| 210 | ScriptEnginePointer scriptEngineFactory(ScriptEngine::Context context, | ||
| 211 | const QString& scriptContents, | ||
| 212 | const QString& fileNameString) { | ||
| 213 | ScriptEngine* engine = new ScriptEngine(context, scriptContents, fileNameString); | ||
| 214 | ScriptEnginePointer engineSP = ScriptEnginePointer(engine, &QObject::deleteLater); | ||
| 215 | auto scriptEngines = DependencyManager::get<ScriptEngines>(); | ||
| 216 | scriptEngines->addScriptEngine(qSharedPointerCast<ScriptEngine>(engineSP)); | ||
| 217 | engine->setScriptEngines(scriptEngines); | ||
| 218 | return engineSP; | ||
| 219 | } | ||
| 220 | |||
| 221 | int ScriptEngine::processLevelMaxRetries { ScriptRequest::MAX_RETRIES }; | ||
| 222 | ScriptEngine::ScriptEngine(Context context, const QString& scriptContents, const QString& fileNameString) : | ||
| 223 | BaseScriptEngine(), | ||
| 224 | _context(context), | ||
| 225 | _scriptContents(scriptContents), | ||
| 226 | _timerFunctionMap(), | ||
| 227 | _fileNameString(fileNameString), | ||
| 228 | _arrayBufferClass(new ArrayBufferClass(this)), | ||
| 229 | _assetScriptingInterface(new AssetScriptingInterface(this)) | ||
| 230 | { | ||
| 231 | switch (_context) { | ||
| 232 | case Context::CLIENT_SCRIPT: | ||
| 233 | _type = Type::CLIENT; | ||
| 234 | break; | ||
| 235 | case Context::ENTITY_CLIENT_SCRIPT: | ||
| 236 | _type = Type::ENTITY_CLIENT; | ||
| 237 | break; | ||
| 238 | case Context::ENTITY_SERVER_SCRIPT: | ||
| 239 | _type = Type::ENTITY_SERVER; | ||
| 240 | break; | ||
| 241 | case Context::AGENT_SCRIPT: | ||
| 242 | _type = Type::AGENT; | ||
| 243 | break; | ||
| 244 | } | ||
| 245 | |||
| 246 | connect(this, &QScriptEngine::signalHandlerException, this, [this](const QScriptValue& exception) { | ||
| 247 | if (hasUncaughtException()) { | ||
| 248 | // the engine's uncaughtException() seems to produce much better stack traces here | ||
| 249 | emit unhandledException(cloneUncaughtException("signalHandlerException")); | ||
| 250 | clearExceptions(); | ||
| 251 | } else { | ||
| 252 | // ... but may not always be available -- so if needed we fallback to the passed exception | ||
| 253 | emit unhandledException(exception); | ||
| 254 | } | ||
| 255 | }, Qt::DirectConnection); | ||
| 256 | |||
| 257 | setProcessEventsInterval(MSECS_PER_SECOND); | ||
| 258 | if (isEntityServerScript()) { | ||
| 259 | qCDebug(scriptengine) << "isEntityServerScript() -- limiting maxRetries to 1"; | ||
| 260 | processLevelMaxRetries = 1; | ||
| 261 | } | ||
| 262 | |||
| 263 | // this is where all unhandled exceptions end up getting logged | ||
| 264 | connect(this, &BaseScriptEngine::unhandledException, this, [this](const QScriptValue& err) { | ||
| 265 | auto output = err.engine() == this ? err : makeError(err); | ||
| 266 | if (!output.property("detail").isValid()) { | ||
| 267 | output.setProperty("detail", "UnhandledException"); | ||
| 268 | } | ||
| 269 | logException(output); | ||
| 270 | }); | ||
| 271 | |||
| 272 | if (_type == Type::ENTITY_CLIENT || _type == Type::ENTITY_SERVER) { | ||
| 273 | QObject::connect(this, &ScriptEngine::update, this, [this]() { | ||
| 274 | // process pending entity script content | ||
| 275 | if (!_contentAvailableQueue.empty() && !(_isFinished || _isStopping)) { | ||
| 276 | EntityScriptContentAvailableMap pending; | ||
| 277 | std::swap(_contentAvailableQueue, pending); | ||
| 278 | for (auto& pair : pending) { | ||
| 279 | auto& args = pair.second; | ||
| 280 | entityScriptContentAvailable(args.entityID, args.scriptOrURL, args.contents, args.isURL, args.success, args.status); | ||
| 281 | } | ||
| 282 | } | ||
| 283 | }); | ||
| 284 | } | ||
| 285 | } | ||
| 286 | |||
| 287 | QString ScriptEngine::getTypeAsString() const { | ||
| 288 | auto value = QVariant::fromValue(_type).toString(); | ||
| 289 | return value.isEmpty() ? "unknown" : value.toLower(); | ||
| 290 | } | ||
| 291 | |||
| 292 | QString ScriptEngine::getContext() const { | ||
| 293 | switch (_context) { | ||
| 294 | case CLIENT_SCRIPT: | ||
| 295 | return "client"; | ||
| 296 | case ENTITY_CLIENT_SCRIPT: | ||
| 297 | return "entity_client"; | ||
| 298 | case ENTITY_SERVER_SCRIPT: | ||
| 299 | return "entity_server"; | ||
| 300 | case AGENT_SCRIPT: | ||
| 301 | return "agent"; | ||
| 302 | default: | ||
| 303 | return "unknown"; | ||
| 304 | } | ||
| 305 | return "unknown"; | ||
| 306 | } | ||
| 307 | |||
| 308 | bool ScriptEngine::isDebugMode() const { | ||
| 309 | #if defined(DEBUG) | ||
| 310 | return true; | ||
| 311 | #else | ||
| 312 | return false; | ||
| 313 | #endif | ||
| 314 | } | ||
| 315 | |||
| 316 | ScriptEngine::~ScriptEngine() {} | ||
| 317 | |||
| 318 | void ScriptEngine::disconnectNonEssentialSignals() { | ||
| 319 | disconnect(); | ||
| 320 | QThread* workerThread; | ||
| 321 | // Ensure the thread should be running, and does exist | ||
| 322 | if (_isRunning && _isThreaded && (workerThread = thread())) { | ||
| 323 | connect(this, &QObject::destroyed, workerThread, &QThread::quit); | ||
| 324 | connect(workerThread, &QThread::finished, workerThread, &QObject::deleteLater); | ||
| 325 | } | ||
| 326 | } | ||
| 327 | |||
| 328 | void ScriptEngine::runInThread() { | ||
| 329 | Q_ASSERT_X(!_isThreaded, "ScriptEngine::runInThread()", "runInThread should not be called more than once"); | ||
| 330 | |||
| 331 | if (_isThreaded) { | ||
| 332 | return; | ||
| 333 | } | ||
| 334 | |||
| 335 | _isThreaded = true; | ||
| 336 | |||
| 337 | // The thread interface cannot live on itself, and we want to move this into the thread, so | ||
| 338 | // the thread cannot have this as a parent. | ||
| 339 | QThread* workerThread = new QThread(); | ||
| 340 | QString name = QString("js:") + getFilename().replace("about:",""); | ||
| 341 | workerThread->setObjectName(name); | ||
| 342 | moveToThread(workerThread); | ||
| 343 | |||
| 344 | // NOTE: If you connect any essential signals for proper shutdown or cleanup of | ||
| 345 | // the script engine, make sure to add code to "reconnect" them to the | ||
| 346 | // disconnectNonEssentialSignals() method | ||
| 347 | connect(workerThread, &QThread::started, this, [this, name] { | ||
| 348 | setThreadName(name.toStdString()); | ||
| 349 | run(); | ||
| 350 | }); | ||
| 351 | connect(this, &QObject::destroyed, workerThread, &QThread::quit); | ||
| 352 | connect(workerThread, &QThread::finished, workerThread, &QObject::deleteLater); | ||
| 353 | |||
| 354 | workerThread->start(); | ||
| 355 | } | ||
| 356 | |||
| 357 | void ScriptEngine::executeOnScriptThread(std::function<void()> function, const Qt::ConnectionType& type ) { | ||
| 358 | if (QThread::currentThread() != thread()) { | ||
| 359 | QMetaObject::invokeMethod(this, "executeOnScriptThread", type, Q_ARG(std::function<void()>, function)); | ||
| 360 | return; | ||
| 361 | } | ||
| 362 | |||
| 363 | function(); | ||
| 364 | } | ||
| 365 | |||
| 366 | void ScriptEngine::waitTillDoneRunning(bool shutdown) { | ||
| 367 | // Engine should be stopped already, but be defensive | ||
| 368 | stop(); | ||
| 369 | |||
| 370 | auto workerThread = thread(); | ||
| 371 | |||
| 372 | if (workerThread == QThread::currentThread()) { | ||
| 373 | qCWarning(scriptengine) << "ScriptEngine::waitTillDoneRunning called, but the script is on the same thread:" << getFilename(); | ||
| 374 | return; | ||
| 375 | } | ||
| 376 | |||
| 377 | if (_isThreaded && workerThread) { | ||
| 378 | // We should never be waiting (blocking) on our own thread | ||
| 379 | assert(workerThread != QThread::currentThread()); | ||
| 380 | |||
| 381 | #if 0 | ||
| 382 | // 26 Feb 2021 - Disabled this OSX-specific code because it causes OSX to crash on shutdown; without this code, OSX | ||
| 383 | // doesn't crash on shutdown. Qt 5.12.3 and Qt 5.15.2. | ||
| 384 | // | ||
| 385 | // On mac, don't call QCoreApplication::processEvents() here. This is to prevent | ||
| 386 | // [NSApplication terminate:] from prematurely destroying the static destructors | ||
| 387 | // while we are waiting for the scripts to shutdown. We will pump the message | ||
| 388 | // queue later in the Application destructor. | ||
| 389 | if (workerThread->isRunning()) { | ||
| 390 | workerThread->quit(); | ||
| 391 | |||
| 392 | if (isEvaluating()) { | ||
| 393 | qCWarning(scriptengine) << "Script Engine has been running too long, aborting:" << getFilename(); | ||
| 394 | abortEvaluation(); | ||
| 395 | } else { | ||
| 396 | auto context = currentContext(); | ||
| 397 | if (context) { | ||
| 398 | qCWarning(scriptengine) << "Script Engine has been running too long, throwing:" << getFilename(); | ||
| 399 | context->throwError("Timed out during shutdown"); | ||
| 400 | } | ||
| 401 | } | ||
| 402 | |||
| 403 | // Wait for the scripting thread to stop running, as | ||
| 404 | // flooding it with aborts/exceptions will persist it longer | ||
| 405 | static const auto MAX_SCRIPT_QUITTING_TIME = 0.5 * MSECS_PER_SECOND; | ||
| 406 | if (!workerThread->wait(MAX_SCRIPT_QUITTING_TIME)) { | ||
| 407 | workerThread->terminate(); | ||
| 408 | } | ||
| 409 | } | ||
| 410 | #else | ||
| 411 | auto startedWaiting = usecTimestampNow(); | ||
| 412 | while (workerThread->isRunning()) { | ||
| 413 | // If the final evaluation takes too long, then tell the script engine to stop running | ||
| 414 | auto elapsedUsecs = usecTimestampNow() - startedWaiting; | ||
| 415 | static const auto MAX_SCRIPT_EVALUATION_TIME = USECS_PER_SECOND; | ||
| 416 | if (elapsedUsecs > MAX_SCRIPT_EVALUATION_TIME) { | ||
| 417 | workerThread->quit(); | ||
| 418 | |||
| 419 | if (isEvaluating()) { | ||
| 420 | qCWarning(scriptengine) << "Script Engine has been running too long, aborting:" << getFilename(); | ||
| 421 | abortEvaluation(); | ||
| 422 | } else { | ||
| 423 | auto context = currentContext(); | ||
| 424 | if (context) { | ||
| 425 | qCWarning(scriptengine) << "Script Engine has been running too long, throwing:" << getFilename(); | ||
| 426 | context->throwError("Timed out during shutdown"); | ||
| 427 | } | ||
| 428 | } | ||
| 429 | |||
| 430 | // Wait for the scripting thread to stop running, as | ||
| 431 | // flooding it with aborts/exceptions will persist it longer | ||
| 432 | static const auto MAX_SCRIPT_QUITTING_TIME = 0.5 * MSECS_PER_SECOND; | ||
| 433 | if (!workerThread->wait(MAX_SCRIPT_QUITTING_TIME)) { | ||
| 434 | workerThread->terminate(); | ||
| 435 | } | ||
| 436 | } | ||
| 437 | |||
| 438 | if (shutdown) { | ||
| 439 | // NOTE: This will be called on the main application thread (among other threads) from stopAllScripts. | ||
| 440 | // The thread will need to continue to process events, because | ||
| 441 | // the scripts will likely need to marshall messages across to the main thread, e.g. | ||
| 442 | // if they access Settings or Menu in any of their shutdown code. So: | ||
| 443 | // Process events for this thread, allowing invokeMethod calls to pass between threads. | ||
| 444 | QCoreApplication::processEvents(); | ||
| 445 | } | ||
| 446 | |||
| 447 | // Avoid a pure busy wait | ||
| 448 | QThread::yieldCurrentThread(); | ||
| 449 | } | ||
| 450 | #endif | ||
| 451 | |||
| 452 | scriptInfoMessage("Script Engine has stopped:" + getFilename()); | ||
| 453 | } | ||
| 454 | } | ||
| 455 | |||
| 456 | QString ScriptEngine::getFilename() const { | ||
| 457 | QStringList fileNameParts = _fileNameString.split("/"); | ||
| 458 | QString lastPart; | ||
| 459 | if (!fileNameParts.isEmpty()) { | ||
| 460 | lastPart = fileNameParts.last(); | ||
| 461 | } | ||
| 462 | return lastPart; | ||
| 463 | } | ||
| 464 | |||
| 465 | bool ScriptEngine::hasValidScriptSuffix(const QString& scriptFileName) { | ||
| 466 | QFileInfo fileInfo(scriptFileName); | ||
| 467 | QString scriptSuffixToLower = fileInfo.completeSuffix().toLower(); | ||
| 468 | return scriptSuffixToLower.contains(QString("js"), Qt::CaseInsensitive); | ||
| 469 | } | ||
| 470 | |||
| 471 | void ScriptEngine::loadURL(const QUrl& scriptURL, bool reload) { | ||
| 472 | if (_isRunning) { | ||
| 473 | return; | ||
| 474 | } | ||
| 475 | |||
| 476 | QUrl url = expandScriptUrl(scriptURL); | ||
| 477 | _fileNameString = url.toString(); | ||
| 478 | _isReloading = reload; | ||
| 479 | |||
| 480 | // Check that script has a supported file extension | ||
| 481 | if (!hasValidScriptSuffix(_fileNameString)) { | ||
| 482 | scriptErrorMessage("File extension of file: " + _fileNameString + " is not a currently supported script type"); | ||
| 483 | emit errorLoadingScript(_fileNameString); | ||
| 484 | return; | ||
| 485 | } | ||
| 486 | |||
| 487 | const auto maxRetries = 0; // for consistency with previous scriptCache->getScript() behavior | ||
| 488 | auto scriptCache = DependencyManager::get<ScriptCache>(); | ||
| 489 | scriptCache->getScriptContents(url.toString(), [this](const QString& url, const QString& scriptContents, bool isURL, bool success, const QString&status) { | ||
| 490 | qCDebug(scriptengine) << "loadURL" << url << status << QThread::currentThread(); | ||
| 491 | if (!success) { | ||
| 492 | scriptErrorMessage("ERROR Loading file (" + status + "):" + url); | ||
| 493 | emit errorLoadingScript(_fileNameString); | ||
| 494 | return; | ||
| 495 | } | ||
| 496 | |||
| 497 | _scriptContents = scriptContents; | ||
| 498 | |||
| 499 | emit scriptLoaded(url); | ||
| 500 | }, reload, maxRetries); | ||
| 501 | } | ||
| 502 | |||
| 503 | void ScriptEngine::scriptErrorMessage(const QString& message) { | ||
| 504 | qCCritical(scriptengine, "[%s] %s", qUtf8Printable(getFilename()), qUtf8Printable(message)); | ||
| 505 | emit errorMessage(message, getFilename()); | ||
| 506 | } | ||
| 507 | |||
| 508 | void ScriptEngine::scriptWarningMessage(const QString& message) { | ||
| 509 | qCWarning(scriptengine, "[%s] %s", qUtf8Printable(getFilename()), qUtf8Printable(message)); | ||
| 510 | emit warningMessage(message, getFilename()); | ||
| 511 | } | ||
| 512 | |||
| 513 | void ScriptEngine::scriptInfoMessage(const QString& message) { | ||
| 514 | qCInfo(scriptengine, "[%s] %s", qUtf8Printable(getFilename()), qUtf8Printable(message)); | ||
| 515 | emit infoMessage(message, getFilename()); | ||
| 516 | } | ||
| 517 | |||
| 518 | void ScriptEngine::scriptPrintedMessage(const QString& message) { | ||
| 519 | qCDebug(scriptengine, "[%s] %s", qUtf8Printable(getFilename()), qUtf8Printable(message)); | ||
| 520 | emit printedMessage(message, getFilename()); | ||
| 521 | } | ||
| 522 | |||
| 523 | void ScriptEngine::clearDebugLogWindow() { | ||
| 524 | emit clearDebugWindow(); | ||
| 525 | } | ||
| 526 | |||
| 527 | // Even though we never pass AnimVariantMap directly to and from javascript, the queued invokeMethod of | ||
| 528 | // callAnimationStateHandler requires that the type be registered. | ||
| 529 | // These two are meaningful, if we ever do want to use them... | ||
| 530 | static QScriptValue animVarMapToScriptValue(QScriptEngine* engine, const AnimVariantMap& parameters) { | ||
| 531 | QStringList unused; | ||
| 532 | return parameters.animVariantMapToScriptValue(engine, unused, false); | ||
| 533 | } | ||
| 534 | static void animVarMapFromScriptValue(const QScriptValue& value, AnimVariantMap& parameters) { | ||
| 535 | parameters.animVariantMapFromScriptValue(value); | ||
| 536 | } | ||
| 537 | // ... while these two are not. But none of the four are ever used. | ||
| 538 | static QScriptValue resultHandlerToScriptValue(QScriptEngine* engine, | ||
| 539 | const AnimVariantResultHandler& resultHandler) { | ||
| 540 | qCCritical(scriptengine) << "Attempt to marshall result handler to javascript"; | ||
| 541 | assert(false); | ||
| 542 | return QScriptValue(); | ||
| 543 | } | ||
| 544 | static void resultHandlerFromScriptValue(const QScriptValue& value, AnimVariantResultHandler& resultHandler) { | ||
| 545 | qCCritical(scriptengine) << "Attempt to marshall result handler from javascript"; | ||
| 546 | assert(false); | ||
| 547 | } | ||
| 548 | |||
| 549 | // Templated qScriptRegisterMetaType fails to compile with raw pointers | ||
| 550 | using ScriptableResourceRawPtr = ScriptableResource*; | ||
| 551 | |||
| 552 | static QScriptValue scriptableResourceToScriptValue(QScriptEngine* engine, | ||
| 553 | const ScriptableResourceRawPtr& resource) { | ||
| 554 | if (!resource) { | ||
| 555 | return QScriptValue(); // probably shutting down | ||
| 556 | } | ||
| 557 | |||
| 558 | // The first script to encounter this resource will track its memory. | ||
| 559 | // In this way, it will be more likely to GC. | ||
| 560 | // This fails in the case that the resource is used across many scripts, but | ||
| 561 | // in that case it would be too difficult to tell which one should track the memory, and | ||
| 562 | // this serves the common case (use in a single script). | ||
| 563 | auto data = resource->getResource(); | ||
| 564 | if (data && !resource->isInScript()) { | ||
| 565 | resource->setInScript(true); | ||
| 566 | QObject::connect(data.data(), SIGNAL(updateSize(qint64)), engine, SLOT(updateMemoryCost(qint64))); | ||
| 567 | } | ||
| 568 | |||
| 569 | auto object = engine->newQObject( | ||
| 570 | const_cast<ScriptableResourceRawPtr>(resource), | ||
| 571 | QScriptEngine::ScriptOwnership, | ||
| 572 | DEFAULT_QOBJECT_WRAP_OPTIONS); | ||
| 573 | return object; | ||
| 574 | } | ||
| 575 | |||
| 576 | static void scriptableResourceFromScriptValue(const QScriptValue& value, ScriptableResourceRawPtr& resource) { | ||
| 577 | resource = static_cast<ScriptableResourceRawPtr>(value.toQObject()); | ||
| 578 | } | ||
| 579 | |||
| 580 | /*@jsdoc | ||
| 581 | * The <code>Resource</code> API provides values that define the possible loading states of a resource. | ||
| 582 | * | ||
| 583 | * @namespace Resource | ||
| 584 | * | ||
| 585 | * @hifi-interface | ||
| 586 | * @hifi-client-entity | ||
| 587 | * @hifi-avatar | ||
| 588 | * @hifi-server-entity | ||
| 589 | * @hifi-assignment-client | ||
| 590 | * | ||
| 591 | * @property {Resource.State} State - The possible loading states of a resource. <em>Read-only.</em> | ||
| 592 | */ | ||
| 593 | static QScriptValue createScriptableResourcePrototype(ScriptEnginePointer engine) { | ||
| 594 | auto prototype = engine->newObject(); | ||
| 595 | |||
| 596 | // Expose enum State to JS/QML via properties | ||
| 597 | QObject* state = new QObject(engine.data()); | ||
| 598 | state->setObjectName("ResourceState"); | ||
| 599 | auto metaEnum = QMetaEnum::fromType<ScriptableResource::State>(); | ||
| 600 | for (int i = 0; i < metaEnum.keyCount(); ++i) { | ||
| 601 | state->setProperty(metaEnum.key(i), metaEnum.value(i)); | ||
| 602 | } | ||
| 603 | |||
| 604 | auto prototypeState = engine->newQObject(state, QScriptEngine::QtOwnership, | ||
| 605 | QScriptEngine::ExcludeDeleteLater | QScriptEngine::ExcludeSlots | QScriptEngine::ExcludeSuperClassMethods); | ||
| 606 | prototype.setProperty("State", prototypeState); | ||
| 607 | |||
| 608 | return prototype; | ||
| 609 | } | ||
| 610 | |||
| 611 | QScriptValue avatarDataToScriptValue(QScriptEngine* engine, ScriptAvatarData* const& in) { | ||
| 612 | return engine->newQObject(in, QScriptEngine::ScriptOwnership, DEFAULT_QOBJECT_WRAP_OPTIONS); | ||
| 613 | } | ||
| 614 | |||
| 615 | void avatarDataFromScriptValue(const QScriptValue& object, ScriptAvatarData*& out) { | ||
| 616 | // This is not implemented because there are no slots/properties that take an AvatarSharedPointer from a script | ||
| 617 | assert(false); | ||
| 618 | out = nullptr; | ||
| 619 | } | ||
| 620 | |||
| 621 | QScriptValue externalResourceBucketToScriptValue(QScriptEngine* engine, ExternalResource::Bucket const& in) { | ||
| 622 | return QScriptValue((int)in); | ||
| 623 | } | ||
| 624 | |||
| 625 | void externalResourceBucketFromScriptValue(const QScriptValue& object, ExternalResource::Bucket& out) { | ||
| 626 | out = static_cast<ExternalResource::Bucket>(object.toInt32()); | ||
| 627 | } | ||
| 628 | |||
| 629 | void ScriptEngine::resetModuleCache(bool deleteScriptCache) { | ||
| 630 | if (QThread::currentThread() != thread()) { | ||
| 631 | executeOnScriptThread([=]() { resetModuleCache(deleteScriptCache); }); | ||
| 632 | return; | ||
| 633 | } | ||
| 634 | auto jsRequire = globalObject().property("Script").property("require"); | ||
| 635 | auto cache = jsRequire.property("cache"); | ||
| 636 | auto cacheMeta = jsRequire.data(); | ||
| 637 | |||
| 638 | if (deleteScriptCache) { | ||
| 639 | QScriptValueIterator it(cache); | ||
| 640 | while (it.hasNext()) { | ||
| 641 | it.next(); | ||
| 642 | if (it.flags() & QScriptValue::SkipInEnumeration) { | ||
| 643 | continue; | ||
| 644 | } | ||
| 645 | qCDebug(scriptengine) << "resetModuleCache(true) -- staging " << it.name() << " for cache reset at next require"; | ||
| 646 | cacheMeta.setProperty(it.name(), true); | ||
| 647 | } | ||
| 648 | } | ||
| 649 | cache = newObject(); | ||
| 650 | if (!cacheMeta.isObject()) { | ||
| 651 | cacheMeta = newObject(); | ||
| 652 | cacheMeta.setProperty("id", "Script.require.cacheMeta"); | ||
| 653 | cacheMeta.setProperty("type", "cacheMeta"); | ||
| 654 | jsRequire.setData(cacheMeta); | ||
| 655 | } | ||
| 656 | cache.setProperty("__created__", (double)QDateTime::currentMSecsSinceEpoch(), QScriptValue::SkipInEnumeration); | ||
| 657 | #if DEBUG_JS_MODULES | ||
| 658 | cache.setProperty("__meta__", cacheMeta, READONLY_HIDDEN_PROP_FLAGS); | ||
| 659 | #endif | ||
| 660 | jsRequire.setProperty("cache", cache, READONLY_PROP_FLAGS); | ||
| 661 | } | ||
| 662 | |||
| 663 | void ScriptEngine::init() { | ||
| 664 | if (_isInitialized) { | ||
| 665 | return; // only initialize once | ||
| 666 | } | ||
| 667 | |||
| 668 | _isInitialized = true; | ||
| 669 | |||
| 670 | auto entityScriptingInterface = DependencyManager::get<EntityScriptingInterface>(); | ||
| 671 | entityScriptingInterface->init(); | ||
| 672 | |||
| 673 | // register various meta-types | ||
| 674 | registerMetaTypes(this); | ||
| 675 | registerMIDIMetaTypes(this); | ||
| 676 | registerEventTypes(this); | ||
| 677 | registerMenuItemProperties(this); | ||
| 678 | registerAnimationTypes(this); | ||
| 679 | registerAvatarTypes(this); | ||
| 680 | registerAudioMetaTypes(this); | ||
| 681 | |||
| 682 | qScriptRegisterMetaType(this, EntityPropertyFlagsToScriptValue, EntityPropertyFlagsFromScriptValue); | ||
| 683 | qScriptRegisterMetaType(this, EntityItemPropertiesToScriptValue, EntityItemPropertiesFromScriptValueHonorReadOnly); | ||
| 684 | qScriptRegisterMetaType(this, EntityPropertyInfoToScriptValue, EntityPropertyInfoFromScriptValue); | ||
| 685 | qScriptRegisterMetaType(this, EntityItemIDtoScriptValue, EntityItemIDfromScriptValue); | ||
| 686 | qScriptRegisterMetaType(this, RayToEntityIntersectionResultToScriptValue, RayToEntityIntersectionResultFromScriptValue); | ||
| 687 | qScriptRegisterMetaType(this, RayToAvatarIntersectionResultToScriptValue, RayToAvatarIntersectionResultFromScriptValue); | ||
| 688 | qScriptRegisterMetaType(this, AvatarEntityMapToScriptValue, AvatarEntityMapFromScriptValue); | ||
| 689 | qScriptRegisterSequenceMetaType<QVector<QUuid>>(this); | ||
| 690 | qScriptRegisterSequenceMetaType<QVector<EntityItemID>>(this); | ||
| 691 | |||
| 692 | qScriptRegisterSequenceMetaType<QVector<glm::vec2>>(this); | ||
| 693 | qScriptRegisterSequenceMetaType<QVector<glm::quat>>(this); | ||
| 694 | qScriptRegisterSequenceMetaType<QVector<QString>>(this); | ||
| 695 | |||
| 696 | QScriptValue xmlHttpRequestConstructorValue = newFunction(XMLHttpRequestClass::constructor); | ||
| 697 | globalObject().setProperty("XMLHttpRequest", xmlHttpRequestConstructorValue); | ||
| 698 | |||
| 699 | QScriptValue webSocketConstructorValue = newFunction(WebSocketClass::constructor); | ||
| 700 | globalObject().setProperty("WebSocket", webSocketConstructorValue); | ||
| 701 | |||
| 702 | /*@jsdoc | ||
| 703 | * Prints a message to the program log and emits {@link Script.printedMessage}. | ||
| 704 | * The message logged is the message values separated by spaces. | ||
| 705 | * <p>Alternatively, you can use {@link Script.print} or one of the {@link console} API methods.</p> | ||
| 706 | * @function print | ||
| 707 | * @param {...*} [message] - The message values to print. | ||
| 708 | */ | ||
| 709 | globalObject().setProperty("print", newFunction(debugPrint)); | ||
| 710 | |||
| 711 | QScriptValue audioEffectOptionsConstructorValue = newFunction(AudioEffectOptions::constructor); | ||
| 712 | globalObject().setProperty("AudioEffectOptions", audioEffectOptionsConstructorValue); | ||
| 713 | |||
| 714 | qScriptRegisterMetaType(this, injectorToScriptValue, injectorFromScriptValue); | ||
| 715 | qScriptRegisterMetaType(this, inputControllerToScriptValue, inputControllerFromScriptValue); | ||
| 716 | qScriptRegisterMetaType(this, avatarDataToScriptValue, avatarDataFromScriptValue); | ||
| 717 | qScriptRegisterMetaType(this, animationDetailsToScriptValue, animationDetailsFromScriptValue); | ||
| 718 | qScriptRegisterMetaType(this, webSocketToScriptValue, webSocketFromScriptValue); | ||
| 719 | qScriptRegisterMetaType(this, qWSCloseCodeToScriptValue, qWSCloseCodeFromScriptValue); | ||
| 720 | qScriptRegisterMetaType(this, wscReadyStateToScriptValue, wscReadyStateFromScriptValue); | ||
| 721 | |||
| 722 | // NOTE: You do not want to end up creating new instances of singletons here. They will be on the ScriptEngine thread | ||
| 723 | // and are likely to be unusable if we "reset" the ScriptEngine by creating a new one (on a whole new thread). | ||
| 724 | |||
| 725 | registerGlobalObject("Script", this); | ||
| 726 | |||
| 727 | { | ||
| 728 | // set up Script.require.resolve and Script.require.cache | ||
| 729 | auto Script = globalObject().property("Script"); | ||
| 730 | auto require = Script.property("require"); | ||
| 731 | auto resolve = Script.property("_requireResolve"); | ||
| 732 | require.setProperty("resolve", resolve, READONLY_PROP_FLAGS); | ||
| 733 | resetModuleCache(); | ||
| 734 | } | ||
| 735 | |||
| 736 | qScriptRegisterMetaType(this, externalResourceBucketToScriptValue, externalResourceBucketFromScriptValue); | ||
| 737 | registerEnum("Script.ExternalPaths", QMetaEnum::fromType<ExternalResource::Bucket>()); | ||
| 738 | |||
| 739 | registerGlobalObject("Audio", DependencyManager::get<AudioScriptingInterface>().data()); | ||
| 740 | |||
| 741 | registerGlobalObject("Midi", DependencyManager::get<Midi>().data()); | ||
| 742 | |||
| 743 | registerGlobalObject("Entities", entityScriptingInterface.data()); | ||
| 744 | registerFunction("Entities", "getMultipleEntityProperties", EntityScriptingInterface::getMultipleEntityProperties); | ||
| 745 | registerGlobalObject("Quat", &_quatLibrary); | ||
| 746 | registerGlobalObject("Vec3", &_vec3Library); | ||
| 747 | registerGlobalObject("Mat4", &_mat4Library); | ||
| 748 | registerGlobalObject("Uuid", &_uuidLibrary); | ||
| 749 | registerGlobalObject("Messages", DependencyManager::get<MessagesClient>().data()); | ||
| 750 | registerGlobalObject("File", new FileScriptingInterface(this)); | ||
| 751 | registerGlobalObject("console", &_consoleScriptingInterface); | ||
| 752 | registerFunction("console", "info", ConsoleScriptingInterface::info, currentContext()->argumentCount()); | ||
| 753 | registerFunction("console", "log", ConsoleScriptingInterface::log, currentContext()->argumentCount()); | ||
| 754 | registerFunction("console", "debug", ConsoleScriptingInterface::debug, currentContext()->argumentCount()); | ||
| 755 | registerFunction("console", "warn", ConsoleScriptingInterface::warn, currentContext()->argumentCount()); | ||
| 756 | registerFunction("console", "error", ConsoleScriptingInterface::error, currentContext()->argumentCount()); | ||
| 757 | registerFunction("console", "exception", ConsoleScriptingInterface::exception, currentContext()->argumentCount()); | ||
| 758 | registerFunction("console", "assert", ConsoleScriptingInterface::assertion, currentContext()->argumentCount()); | ||
| 759 | registerFunction("console", "group", ConsoleScriptingInterface::group, 1); | ||
| 760 | registerFunction("console", "groupCollapsed", ConsoleScriptingInterface::groupCollapsed, 1); | ||
| 761 | registerFunction("console", "groupEnd", ConsoleScriptingInterface::groupEnd, 0); | ||
| 762 | |||
| 763 | qScriptRegisterMetaType(this, animVarMapToScriptValue, animVarMapFromScriptValue); | ||
| 764 | qScriptRegisterMetaType(this, resultHandlerToScriptValue, resultHandlerFromScriptValue); | ||
| 765 | |||
| 766 | // Scriptable cache access | ||
| 767 | auto resourcePrototype = createScriptableResourcePrototype(qSharedPointerCast<ScriptEngine>(sharedFromThis())); | ||
| 768 | globalObject().setProperty("Resource", resourcePrototype); | ||
| 769 | setDefaultPrototype(qMetaTypeId<ScriptableResource*>(), resourcePrototype); | ||
| 770 | qScriptRegisterMetaType(this, scriptableResourceToScriptValue, scriptableResourceFromScriptValue); | ||
| 771 | |||
| 772 | // constants | ||
| 773 | globalObject().setProperty("TREE_SCALE", newVariant(QVariant(TREE_SCALE))); | ||
| 774 | |||
| 775 | registerGlobalObject("Assets", _assetScriptingInterface); | ||
| 776 | registerGlobalObject("Resources", DependencyManager::get<ResourceScriptingInterface>().data()); | ||
| 777 | |||
| 778 | registerGlobalObject("DebugDraw", &DebugDraw::getInstance()); | ||
| 779 | |||
| 780 | registerGlobalObject("Model", new ModelScriptingInterface(this)); | ||
| 781 | qScriptRegisterMetaType(this, meshToScriptValue, meshFromScriptValue); | ||
| 782 | qScriptRegisterMetaType(this, meshesToScriptValue, meshesFromScriptValue); | ||
| 783 | |||
| 784 | registerGlobalObject("UserActivityLogger", DependencyManager::get<UserActivityLoggerScriptingInterface>().data()); | ||
| 785 | |||
| 786 | #if DEV_BUILD || PR_BUILD | ||
| 787 | registerGlobalObject("StackTest", new StackTestScriptingInterface(this)); | ||
| 788 | #endif | ||
| 789 | |||
| 790 | globalObject().setProperty("KALILA", "isWaifu"); | ||
| 791 | globalObject().setProperty("Kute", newFunction([](QScriptContext* context, QScriptEngine* engine) -> QScriptValue { | ||
| 792 | return context->argument(0).toString().toLower() == "kalila" ? true : false; | ||
| 793 | })); | ||
| 794 | } | ||
| 795 | |||
| 796 | void ScriptEngine::registerEnum(const QString& enumName, QMetaEnum newEnum) { | ||
| 797 | if (!newEnum.isValid()) { | ||
| 798 | qCCritical(scriptengine) << "registerEnum called on invalid enum with name " << enumName; | ||
| 799 | return; | ||
| 800 | } | ||
| 801 | |||
| 802 | for (int i = 0; i < newEnum.keyCount(); i++) { | ||
| 803 | const char* keyName = newEnum.key(i); | ||
| 804 | QString fullName = enumName + "." + keyName; | ||
| 805 | registerValue(fullName, newEnum.keyToValue(keyName)); | ||
| 806 | } | ||
| 807 | } | ||
| 808 | |||
| 809 | void ScriptEngine::registerValue(const QString& valueName, QScriptValue value) { | ||
| 810 | if (QThread::currentThread() != thread()) { | ||
| 811 | #ifdef THREAD_DEBUGGING | ||
| 812 | qCDebug(scriptengine) << "*** WARNING *** ScriptEngine::registerValue() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "]"; | ||
| 813 | #endif | ||
| 814 | QMetaObject::invokeMethod(this, "registerValue", | ||
| 815 | Q_ARG(const QString&, valueName), | ||
| 816 | Q_ARG(QScriptValue, value)); | ||
| 817 | return; | ||
| 818 | } | ||
| 819 | |||
| 820 | QStringList pathToValue = valueName.split("."); | ||
| 821 | int partsToGo = pathToValue.length(); | ||
| 822 | QScriptValue partObject = globalObject(); | ||
| 823 | |||
| 824 | for (const auto& pathPart : pathToValue) { | ||
| 825 | partsToGo--; | ||
| 826 | if (!partObject.property(pathPart).isValid()) { | ||
| 827 | if (partsToGo > 0) { | ||
| 828 | //QObject *object = new QObject; | ||
| 829 | QScriptValue partValue = newArray(); //newQObject(object, QScriptEngine::ScriptOwnership); | ||
| 830 | partObject.setProperty(pathPart, partValue); | ||
| 831 | } else { | ||
| 832 | partObject.setProperty(pathPart, value); | ||
| 833 | } | ||
| 834 | } | ||
| 835 | partObject = partObject.property(pathPart); | ||
| 836 | } | ||
| 837 | } | ||
| 838 | |||
| 839 | void ScriptEngine::registerGlobalObject(const QString& name, QObject* object) { | ||
| 840 | if (QThread::currentThread() != thread()) { | ||
| 841 | #ifdef THREAD_DEBUGGING | ||
| 842 | qCDebug(scriptengine) << "*** WARNING *** ScriptEngine::registerGlobalObject() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] name:" << name; | ||
| 843 | #endif | ||
| 844 | QMetaObject::invokeMethod(this, "registerGlobalObject", | ||
| 845 | Q_ARG(const QString&, name), | ||
| 846 | Q_ARG(QObject*, object)); | ||
| 847 | return; | ||
| 848 | } | ||
| 849 | #ifdef THREAD_DEBUGGING | ||
| 850 | qCDebug(scriptengine) << "ScriptEngine::registerGlobalObject() called on thread [" << QThread::currentThread() << "] name:" << name; | ||
| 851 | #endif | ||
| 852 | |||
| 853 | if (!globalObject().property(name).isValid()) { | ||
| 854 | if (object) { | ||
| 855 | QScriptValue value = newQObject(object, QScriptEngine::QtOwnership, DEFAULT_QOBJECT_WRAP_OPTIONS); | ||
| 856 | globalObject().setProperty(name, value); | ||
| 857 | } else { | ||
| 858 | globalObject().setProperty(name, QScriptValue()); | ||
| 859 | } | ||
| 860 | } | ||
| 861 | } | ||
| 862 | |||
| 863 | void ScriptEngine::registerFunction(const QString& name, QScriptEngine::FunctionSignature functionSignature, int numArguments) { | ||
| 864 | if (QThread::currentThread() != thread()) { | ||
| 865 | #ifdef THREAD_DEBUGGING | ||
| 866 | qCDebug(scriptengine) << "*** WARNING *** ScriptEngine::registerFunction() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] name:" << name; | ||
| 867 | #endif | ||
| 868 | QMetaObject::invokeMethod(this, "registerFunction", | ||
| 869 | Q_ARG(const QString&, name), | ||
| 870 | Q_ARG(QScriptEngine::FunctionSignature, functionSignature), | ||
| 871 | Q_ARG(int, numArguments)); | ||
| 872 | return; | ||
| 873 | } | ||
| 874 | #ifdef THREAD_DEBUGGING | ||
| 875 | qCDebug(scriptengine) << "ScriptEngine::registerFunction() called on thread [" << QThread::currentThread() << "] name:" << name; | ||
| 876 | #endif | ||
| 877 | |||
| 878 | QScriptValue scriptFun = newFunction(functionSignature, numArguments); | ||
| 879 | globalObject().setProperty(name, scriptFun); | ||
| 880 | } | ||
| 881 | |||
| 882 | void ScriptEngine::registerFunction(const QString& parent, const QString& name, QScriptEngine::FunctionSignature functionSignature, int numArguments) { | ||
| 883 | if (QThread::currentThread() != thread()) { | ||
| 884 | #ifdef THREAD_DEBUGGING | ||
| 885 | qCDebug(scriptengine) << "*** WARNING *** ScriptEngine::registerFunction() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] parent:" << parent << "name:" << name; | ||
| 886 | #endif | ||
| 887 | QMetaObject::invokeMethod(this, "registerFunction", | ||
| 888 | Q_ARG(const QString&, name), | ||
| 889 | Q_ARG(QScriptEngine::FunctionSignature, functionSignature), | ||
| 890 | Q_ARG(int, numArguments)); | ||
| 891 | return; | ||
| 892 | } | ||
| 893 | #ifdef THREAD_DEBUGGING | ||
| 894 | qCDebug(scriptengine) << "ScriptEngine::registerFunction() called on thread [" << QThread::currentThread() << "] parent:" << parent << "name:" << name; | ||
| 895 | #endif | ||
| 896 | |||
| 897 | QScriptValue object = globalObject().property(parent); | ||
| 898 | if (object.isValid()) { | ||
| 899 | QScriptValue scriptFun = newFunction(functionSignature, numArguments); | ||
| 900 | object.setProperty(name, scriptFun); | ||
| 901 | } | ||
| 902 | } | ||
| 903 | |||
| 904 | void ScriptEngine::registerGetterSetter(const QString& name, QScriptEngine::FunctionSignature getter, | ||
| 905 | QScriptEngine::FunctionSignature setter, const QString& parent) { | ||
| 906 | if (QThread::currentThread() != thread()) { | ||
| 907 | #ifdef THREAD_DEBUGGING | ||
| 908 | qCDebug(scriptengine) << "*** WARNING *** ScriptEngine::registerGetterSetter() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] " | ||
| 909 | " name:" << name << "parent:" << parent; | ||
| 910 | #endif | ||
| 911 | QMetaObject::invokeMethod(this, "registerGetterSetter", | ||
| 912 | Q_ARG(const QString&, name), | ||
| 913 | Q_ARG(QScriptEngine::FunctionSignature, getter), | ||
| 914 | Q_ARG(QScriptEngine::FunctionSignature, setter), | ||
| 915 | Q_ARG(const QString&, parent)); | ||
| 916 | return; | ||
| 917 | } | ||
| 918 | #ifdef THREAD_DEBUGGING | ||
| 919 | qCDebug(scriptengine) << "ScriptEngine::registerGetterSetter() called on thread [" << QThread::currentThread() << "] name:" << name << "parent:" << parent; | ||
| 920 | #endif | ||
| 921 | |||
| 922 | QScriptValue setterFunction = newFunction(setter, 1); | ||
| 923 | QScriptValue getterFunction = newFunction(getter); | ||
| 924 | |||
| 925 | if (!parent.isNull() && !parent.isEmpty()) { | ||
| 926 | QScriptValue object = globalObject().property(parent); | ||
| 927 | if (object.isValid()) { | ||
| 928 | object.setProperty(name, setterFunction, QScriptValue::PropertySetter); | ||
| 929 | object.setProperty(name, getterFunction, QScriptValue::PropertyGetter); | ||
| 930 | } | ||
| 931 | } else { | ||
| 932 | globalObject().setProperty(name, setterFunction, QScriptValue::PropertySetter); | ||
| 933 | globalObject().setProperty(name, getterFunction, QScriptValue::PropertyGetter); | ||
| 934 | } | ||
| 935 | } | ||
| 936 | |||
| 937 | // Unregister the handlers for this eventName and entityID. | ||
| 938 | void ScriptEngine::removeEventHandler(const EntityItemID& entityID, const QString& eventName, QScriptValue handler) { | ||
| 939 | if (QThread::currentThread() != thread()) { | ||
| 940 | #ifdef THREAD_DEBUGGING | ||
| 941 | qCDebug(scriptengine) << "*** WARNING *** ScriptEngine::removeEventHandler() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] " | ||
| 942 | "entityID:" << entityID << " eventName:" << eventName; | ||
| 943 | #endif | ||
| 944 | QMetaObject::invokeMethod(this, "removeEventHandler", | ||
| 945 | Q_ARG(const EntityItemID&, entityID), | ||
| 946 | Q_ARG(const QString&, eventName), | ||
| 947 | Q_ARG(QScriptValue, handler)); | ||
| 948 | return; | ||
| 949 | } | ||
| 950 | #ifdef THREAD_DEBUGGING | ||
| 951 | qCDebug(scriptengine) << "ScriptEngine::removeEventHandler() called on thread [" << QThread::currentThread() << "] entityID:" << entityID << " eventName : " << eventName; | ||
| 952 | #endif | ||
| 953 | |||
| 954 | if (!_registeredHandlers.contains(entityID)) { | ||
| 955 | return; | ||
| 956 | } | ||
| 957 | RegisteredEventHandlers& handlersOnEntity = _registeredHandlers[entityID]; | ||
| 958 | CallbackList& handlersForEvent = handlersOnEntity[eventName]; | ||
| 959 | // QScriptValue does not have operator==(), so we can't use QList::removeOne and friends. So iterate. | ||
| 960 | for (int i = 0; i < handlersForEvent.count(); ++i) { | ||
| 961 | if (handlersForEvent[i].function.equals(handler)) { | ||
| 962 | handlersForEvent.removeAt(i); | ||
| 963 | return; // Design choice: since comparison is relatively expensive, just remove the first matching handler. | ||
| 964 | } | ||
| 965 | } | ||
| 966 | } | ||
| 967 | // Register the handler. | ||
| 968 | void ScriptEngine::addEventHandler(const EntityItemID& entityID, const QString& eventName, QScriptValue handler) { | ||
| 969 | if (QThread::currentThread() != thread()) { | ||
| 970 | #ifdef THREAD_DEBUGGING | ||
| 971 | qCDebug(scriptengine) << "*** WARNING *** ScriptEngine::addEventHandler() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] " | ||
| 972 | "entityID:" << entityID << " eventName:" << eventName; | ||
| 973 | #endif | ||
| 974 | |||
| 975 | QMetaObject::invokeMethod(this, "addEventHandler", | ||
| 976 | Q_ARG(const EntityItemID&, entityID), | ||
| 977 | Q_ARG(const QString&, eventName), | ||
| 978 | Q_ARG(QScriptValue, handler)); | ||
| 979 | return; | ||
| 980 | } | ||
| 981 | #ifdef THREAD_DEBUGGING | ||
| 982 | qCDebug(scriptengine) << "ScriptEngine::addEventHandler() called on thread [" << QThread::currentThread() << "] entityID:" << entityID << " eventName : " << eventName; | ||
| 983 | #endif | ||
| 984 | |||
| 985 | if (_registeredHandlers.count() == 0) { // First time any per-entity handler has been added in this script... | ||
| 986 | // Connect up ALL the handlers to the global entities object's signals. | ||
| 987 | // (We could go signal by signal, or even handler by handler, but I don't think the efficiency is worth the complexity.) | ||
| 988 | auto entities = DependencyManager::get<EntityScriptingInterface>(); | ||
| 989 | // Bug? These handlers are deleted when entityID is deleted, which is nice. | ||
| 990 | // But if they are created by an entity script on a different entity, should they also be deleted when the entity script unloads? | ||
| 991 | // E.g., suppose a bow has an entity script that causes arrows to be created with a potential lifetime greater than the bow, | ||
| 992 | // and that the entity script adds (e.g., collision) handlers to the arrows. Should those handlers fire if the bow is unloaded? | ||
| 993 | // Also, what about when the entity script is REloaded? | ||
| 994 | // For now, we are leaving them around. Changing that would require some non-trivial digging around to find the | ||
| 995 | // handlers that were added while a given currentEntityIdentifier was in place. I don't think this is dangerous. Just perhaps unexpected. -HRS | ||
| 996 | connect(entities.data(), &EntityScriptingInterface::deletingEntity, this, [this](const EntityItemID& entityID) { | ||
| 997 | _registeredHandlers.remove(entityID); | ||
| 998 | }); | ||
| 999 | |||
| 1000 | // Two common cases of event handler, differing only in argument signature. | ||
| 1001 | |||
| 1002 | /*@jsdoc | ||
| 1003 | * Called when an entity event occurs on an entity as registered with {@link Script.addEventHandler}. | ||
| 1004 | * @callback Script~entityEventCallback | ||
| 1005 | * @param {Uuid} entityID - The ID of the entity the event has occured on. | ||
| 1006 | */ | ||
| 1007 | using SingleEntityHandler = std::function<void(const EntityItemID&)>; | ||
| 1008 | auto makeSingleEntityHandler = [this](QString eventName) -> SingleEntityHandler { | ||
| 1009 | return [this, eventName](const EntityItemID& entityItemID) { | ||
| 1010 | forwardHandlerCall(entityItemID, eventName, { entityItemID.toScriptValue(this) }); | ||
| 1011 | }; | ||
| 1012 | }; | ||
| 1013 | |||
| 1014 | /*@jsdoc | ||
| 1015 | * Called when a pointer event occurs on an entity as registered with {@link Script.addEventHandler}. | ||
| 1016 | * @callback Script~pointerEventCallback | ||
| 1017 | * @param {Uuid} entityID - The ID of the entity the event has occurred on. | ||
| 1018 | * @param {PointerEvent} pointerEvent - Details of the event. | ||
| 1019 | */ | ||
| 1020 | using PointerHandler = std::function<void(const EntityItemID&, const PointerEvent&)>; | ||
| 1021 | auto makePointerHandler = [this](QString eventName) -> PointerHandler { | ||
| 1022 | return [this, eventName](const EntityItemID& entityItemID, const PointerEvent& event) { | ||
| 1023 | if (!EntityTree::areEntityClicksCaptured()) { | ||
| 1024 | forwardHandlerCall(entityItemID, eventName, { entityItemID.toScriptValue(this), event.toScriptValue(this) }); | ||
| 1025 | } | ||
| 1026 | }; | ||
| 1027 | }; | ||
| 1028 | |||
| 1029 | /*@jsdoc | ||
| 1030 | * Called when a collision event occurs on an entity as registered with {@link Script.addEventHandler}. | ||
| 1031 | * @callback Script~collisionEventCallback | ||
| 1032 | * @param {Uuid} entityA - The ID of one entity in the collision. | ||
| 1033 | * @param {Uuid} entityB - The ID of the other entity in the collision. | ||
| 1034 | * @param {Collision} collisionEvent - Details of the collision. | ||
| 1035 | */ | ||
| 1036 | using CollisionHandler = std::function<void(const EntityItemID&, const EntityItemID&, const Collision&)>; | ||
| 1037 | auto makeCollisionHandler = [this](QString eventName) -> CollisionHandler { | ||
| 1038 | return [this, eventName](const EntityItemID& idA, const EntityItemID& idB, const Collision& collision) { | ||
| 1039 | forwardHandlerCall(idA, eventName, { idA.toScriptValue(this), idB.toScriptValue(this), | ||
| 1040 | collisionToScriptValue(this, collision) }); | ||
| 1041 | }; | ||
| 1042 | }; | ||
| 1043 | |||
| 1044 | /*@jsdoc | ||
| 1045 | * <p>The name of an entity event. When the entity event occurs, any function that has been registered for that event | ||
| 1046 | * via {@link Script.addEventHandler} is called with parameters per the entity event.</p> | ||
| 1047 | * <table> | ||
| 1048 | * <thead> | ||
| 1049 | * <tr><th>Event Name</th><th>Callback Type</th><th>Entity Event</th></tr> | ||
| 1050 | * </thead> | ||
| 1051 | * <tbody> | ||
| 1052 | * <tr><td><code>"enterEntity"</code></td><td>{@link Script~entityEventCallback|entityEventCallback}</td> | ||
| 1053 | * <td>{@link Entities.enterEntity}</td></tr> | ||
| 1054 | * <tr><td><code>"leaveEntity"</code></td><td>{@link Script~entityEventCallback|entityEventCallback}</td> | ||
| 1055 | * <td>{@link Entities.leaveEntity}</td></tr> | ||
| 1056 | * <tr><td><code>"mousePressOnEntity"</code></td><td>{@link Script~pointerEventCallback|pointerEventCallback}</td> | ||
| 1057 | * <td>{@link Entities.mousePressOnEntity}</td></tr> | ||
| 1058 | * <tr><td><code>"mouseMoveOnEntity"</code></td><td>{@link Script~pointerEventCallback|pointerEventCallback}</td> | ||
| 1059 | * <td>{@link Entities.mouseMoveOnEntity}</td></tr> | ||
| 1060 | * <tr><td><code>"mouseReleaseOnEntity"</code></td><td>{@link Script~pointerEventCallback|pointerEventCallback}</td> | ||
| 1061 | * <td>{@link Entities.mouseReleaseOnEntity}</td></tr> | ||
| 1062 | * <tr><td><code>"clickDownOnEntity"</code></td><td>{@link Script~pointerEventCallback|pointerEventCallback}</td> | ||
| 1063 | * <td>{@link Entities.clickDownOnEntity}</td></tr> | ||
| 1064 | * <tr><td><code>"holdingClickOnEntity"</code></td><td>{@link Script~pointerEventCallback|pointerEventCallback}</td> | ||
| 1065 | * <td>{@link Entities.holdingClickOnEntity}</td></tr> | ||
| 1066 | * <tr><td><code>"clickReleaseOnEntity"</code></td><td>{@link Script~pointerEventCallback|pointerEventCallback}</td> | ||
| 1067 | * <td>{@link Entities.clickReleaseOnEntity}</td></tr> | ||
| 1068 | * <tr><td><code>"hoverEnterEntity"</code></td><td>{@link Script~pointerEventCallback|pointerEventCallback}</td> | ||
| 1069 | * <td>{@link Entities.hoverEnterEntity}</td></tr> | ||
| 1070 | * <tr><td><code>"hoverOverEntity"</code></td><td>{@link Script~pointerEventCallback|pointerEventCallback}</td> | ||
| 1071 | * <td>{@link Entities.hoverOverEntity}</td></tr> | ||
| 1072 | * <tr><td><code>"hoverLeaveEntity"</code></td><td>{@link Script~pointerEventCallback|pointerEventCallback}</td> | ||
| 1073 | * <td>{@link Entities.hoverLeaveEntity}</td></tr> | ||
| 1074 | * <tr><td><code>"collisionWithEntity"</code><td>{@link Script~collisionEventCallback|collisionEventCallback}</td> | ||
| 1075 | * </td><td>{@link Entities.collisionWithEntity}</td></tr> | ||
| 1076 | * </tbody> | ||
| 1077 | * </table> | ||
| 1078 | * @typedef {string} Script.EntityEvent | ||
| 1079 | */ | ||
| 1080 | connect(entities.data(), &EntityScriptingInterface::enterEntity, this, makeSingleEntityHandler("enterEntity")); | ||
| 1081 | connect(entities.data(), &EntityScriptingInterface::leaveEntity, this, makeSingleEntityHandler("leaveEntity")); | ||
| 1082 | |||
| 1083 | connect(entities.data(), &EntityScriptingInterface::mousePressOnEntity, this, makePointerHandler("mousePressOnEntity")); | ||
| 1084 | connect(entities.data(), &EntityScriptingInterface::mouseMoveOnEntity, this, makePointerHandler("mouseMoveOnEntity")); | ||
| 1085 | connect(entities.data(), &EntityScriptingInterface::mouseReleaseOnEntity, this, makePointerHandler("mouseReleaseOnEntity")); | ||
| 1086 | |||
| 1087 | connect(entities.data(), &EntityScriptingInterface::clickDownOnEntity, this, makePointerHandler("clickDownOnEntity")); | ||
| 1088 | connect(entities.data(), &EntityScriptingInterface::holdingClickOnEntity, this, makePointerHandler("holdingClickOnEntity")); | ||
| 1089 | connect(entities.data(), &EntityScriptingInterface::clickReleaseOnEntity, this, makePointerHandler("clickReleaseOnEntity")); | ||
| 1090 | |||
| 1091 | connect(entities.data(), &EntityScriptingInterface::hoverEnterEntity, this, makePointerHandler("hoverEnterEntity")); | ||
| 1092 | connect(entities.data(), &EntityScriptingInterface::hoverOverEntity, this, makePointerHandler("hoverOverEntity")); | ||
| 1093 | connect(entities.data(), &EntityScriptingInterface::hoverLeaveEntity, this, makePointerHandler("hoverLeaveEntity")); | ||
| 1094 | |||
| 1095 | connect(entities.data(), &EntityScriptingInterface::collisionWithEntity, this, makeCollisionHandler("collisionWithEntity")); | ||
| 1096 | } | ||
| 1097 | if (!_registeredHandlers.contains(entityID)) { | ||
| 1098 | _registeredHandlers[entityID] = RegisteredEventHandlers(); | ||
| 1099 | } | ||
| 1100 | CallbackList& handlersForEvent = _registeredHandlers[entityID][eventName]; | ||
| 1101 | CallbackData handlerData = { handler, currentEntityIdentifier, currentSandboxURL }; | ||
| 1102 | handlersForEvent << handlerData; // Note that the same handler can be added many times. See removeEntityEventHandler(). | ||
| 1103 | } | ||
| 1104 | |||
| 1105 | // this is not redundant -- the version in BaseScriptEngine is specifically not Q_INVOKABLE | ||
| 1106 | QScriptValue ScriptEngine::evaluateInClosure(const QScriptValue& closure, const QScriptProgram& program) { | ||
| 1107 | return BaseScriptEngine::evaluateInClosure(closure, program); | ||
| 1108 | } | ||
| 1109 | |||
| 1110 | QScriptValue ScriptEngine::evaluate(const QString& sourceCode, const QString& fileName, int lineNumber) { | ||
| 1111 | QSharedPointer<ScriptEngines> scriptEngines(_scriptEngines); | ||
| 1112 | if (!scriptEngines || scriptEngines->isStopped()) { | ||
| 1113 | return QScriptValue(); // bail early | ||
| 1114 | } | ||
| 1115 | |||
| 1116 | if (QThread::currentThread() != thread()) { | ||
| 1117 | QScriptValue result; | ||
| 1118 | #ifdef THREAD_DEBUGGING | ||
| 1119 | qCDebug(scriptengine) << "*** WARNING *** ScriptEngine::evaluate() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] " | ||
| 1120 | "sourceCode:" << sourceCode << " fileName:" << fileName << "lineNumber:" << lineNumber; | ||
| 1121 | #endif | ||
| 1122 | BLOCKING_INVOKE_METHOD(this, "evaluate", | ||
| 1123 | Q_RETURN_ARG(QScriptValue, result), | ||
| 1124 | Q_ARG(const QString&, sourceCode), | ||
| 1125 | Q_ARG(const QString&, fileName), | ||
| 1126 | Q_ARG(int, lineNumber)); | ||
| 1127 | return result; | ||
| 1128 | } | ||
| 1129 | |||
| 1130 | // Check syntax | ||
| 1131 | auto syntaxError = lintScript(sourceCode, fileName); | ||
| 1132 | if (syntaxError.isError()) { | ||
| 1133 | if (!isEvaluating()) { | ||
| 1134 | syntaxError.setProperty("detail", "evaluate"); | ||
| 1135 | } | ||
| 1136 | raiseException(syntaxError); | ||
| 1137 | maybeEmitUncaughtException("lint"); | ||
| 1138 | return syntaxError; | ||
| 1139 | } | ||
| 1140 | QScriptProgram program { sourceCode, fileName, lineNumber }; | ||
| 1141 | if (program.isNull()) { | ||
| 1142 | // can this happen? | ||
| 1143 | auto err = makeError("could not create QScriptProgram for " + fileName); | ||
| 1144 | raiseException(err); | ||
| 1145 | maybeEmitUncaughtException("compile"); | ||
| 1146 | return err; | ||
| 1147 | } | ||
| 1148 | |||
| 1149 | QScriptValue result; | ||
| 1150 | { | ||
| 1151 | result = BaseScriptEngine::evaluate(program); | ||
| 1152 | maybeEmitUncaughtException("evaluate"); | ||
| 1153 | } | ||
| 1154 | return result; | ||
| 1155 | } | ||
| 1156 | |||
| 1157 | void ScriptEngine::run() { | ||
| 1158 | if (QThread::currentThread() != qApp->thread() && _context == Context::CLIENT_SCRIPT) { | ||
| 1159 | // Flag that we're allowed to access local HTML files on UI created from C++ calls on this thread | ||
| 1160 | // (because we're a client script) | ||
| 1161 | hifi::scripting::setLocalAccessSafeThread(true); | ||
| 1162 | } | ||
| 1163 | |||
| 1164 | auto filenameParts = _fileNameString.split("/"); | ||
| 1165 | auto name = filenameParts.size() > 0 ? filenameParts[filenameParts.size() - 1] : "unknown"; | ||
| 1166 | PROFILE_SET_THREAD_NAME("Script: " + name); | ||
| 1167 | |||
| 1168 | QSharedPointer<ScriptEngines> scriptEngines(_scriptEngines); | ||
| 1169 | if (!scriptEngines || scriptEngines->isStopped()) { | ||
| 1170 | return; // bail early - avoid setting state in init(), as evaluate() will bail too | ||
| 1171 | } | ||
| 1172 | |||
| 1173 | scriptInfoMessage("Script Engine starting:" + getFilename()); | ||
| 1174 | |||
| 1175 | if (!_isInitialized) { | ||
| 1176 | init(); | ||
| 1177 | } | ||
| 1178 | |||
| 1179 | _isRunning = true; | ||
| 1180 | emit runningStateChanged(); | ||
| 1181 | |||
| 1182 | { | ||
| 1183 | PROFILE_RANGE(script, _fileNameString); | ||
| 1184 | evaluate(_scriptContents, _fileNameString); | ||
| 1185 | maybeEmitUncaughtException(__FUNCTION__); | ||
| 1186 | } | ||
| 1187 | #ifdef _WIN32 | ||
| 1188 | // VS13 does not sleep_until unless it uses the system_clock, see: | ||
| 1189 | // https://www.reddit.com/r/cpp_questions/comments/3o71ic/sleep_until_not_working_with_a_time_pointsteady/ | ||
| 1190 | using clock = std::chrono::system_clock; | ||
| 1191 | #else | ||
| 1192 | using clock = std::chrono::high_resolution_clock; | ||
| 1193 | #endif | ||
| 1194 | |||
| 1195 | clock::time_point startTime = clock::now(); | ||
| 1196 | int thisFrame = 0; | ||
| 1197 | |||
| 1198 | auto nodeList = DependencyManager::get<NodeList>(); | ||
| 1199 | auto entityScriptingInterface = DependencyManager::get<EntityScriptingInterface>(); | ||
| 1200 | |||
| 1201 | _lastUpdate = usecTimestampNow(); | ||
| 1202 | |||
| 1203 | std::chrono::microseconds totalUpdates(0); | ||
| 1204 | |||
| 1205 | // TODO: Integrate this with signals/slots instead of reimplementing throttling for ScriptEngine | ||
| 1206 | while (!_isFinished) { | ||
| 1207 | auto beforeSleep = clock::now(); | ||
| 1208 | |||
| 1209 | // Throttle to SCRIPT_FPS | ||
| 1210 | // We'd like to try to keep the script at a solid SCRIPT_FPS update rate. And so we will | ||
| 1211 | // calculate a sleepUntil to be the time from our start time until the original target | ||
| 1212 | // sleepUntil for this frame. This approach will allow us to "catch up" in the event | ||
| 1213 | // that some of our script udpates/frames take a little bit longer than the target average | ||
| 1214 | // to execute. | ||
| 1215 | // NOTE: if we go to variable SCRIPT_FPS, then we will need to reconsider this approach | ||
| 1216 | const std::chrono::microseconds TARGET_SCRIPT_FRAME_DURATION(USECS_PER_SECOND / SCRIPT_FPS + 1); | ||
| 1217 | clock::time_point targetSleepUntil(startTime + (thisFrame++ * TARGET_SCRIPT_FRAME_DURATION)); | ||
| 1218 | |||
| 1219 | // However, if our sleepUntil is not at least our average update and timer execution time | ||
| 1220 | // into the future it means our script is taking too long in its updates, and we want to | ||
| 1221 | // punish the script a little bit. So we will force the sleepUntil to be at least our | ||
| 1222 | // averageUpdate + averageTimerPerFrame time into the future. | ||
| 1223 | auto averageUpdate = totalUpdates / thisFrame; | ||
| 1224 | auto averageTimerPerFrame = _totalTimerExecution / thisFrame; | ||
| 1225 | auto averageTimerAndUpdate = averageUpdate + averageTimerPerFrame; | ||
| 1226 | auto sleepUntil = std::max(targetSleepUntil, beforeSleep + averageTimerAndUpdate); | ||
| 1227 | |||
| 1228 | // We don't want to actually sleep for too long, because it causes our scripts to hang | ||
| 1229 | // on shutdown and stop... so we want to loop and sleep until we've spent our time in | ||
| 1230 | // purgatory, constantly checking to see if our script was asked to end | ||
| 1231 | bool processedEvents = false; | ||
| 1232 | if (!_isFinished) { | ||
| 1233 | PROFILE_RANGE(script, "processEvents-sleep"); | ||
| 1234 | std::chrono::milliseconds sleepFor = | ||
| 1235 | std::chrono::duration_cast<std::chrono::milliseconds>(sleepUntil - clock::now()); | ||
| 1236 | if (sleepFor > std::chrono::milliseconds(0)) { | ||
| 1237 | QEventLoop loop; | ||
| 1238 | QTimer timer; | ||
| 1239 | timer.setSingleShot(true); | ||
| 1240 | connect(&timer, SIGNAL(timeout()), &loop, SLOT(quit())); | ||
| 1241 | timer.start(sleepFor.count()); | ||
| 1242 | loop.exec(); | ||
| 1243 | } else { | ||
| 1244 | QCoreApplication::processEvents(); | ||
| 1245 | } | ||
| 1246 | processedEvents = true; | ||
| 1247 | } | ||
| 1248 | |||
| 1249 | PROFILE_RANGE(script, "ScriptMainLoop"); | ||
| 1250 | |||
| 1251 | #ifdef SCRIPT_DELAY_DEBUG | ||
| 1252 | { | ||
| 1253 | auto actuallySleptUntil = clock::now(); | ||
| 1254 | uint64_t seconds = std::chrono::duration_cast<std::chrono::seconds>(actuallySleptUntil - startTime).count(); | ||
| 1255 | if (seconds > 0) { // avoid division by zero and time travel | ||
| 1256 | uint64_t fps = thisFrame / seconds; | ||
| 1257 | // Overreporting artificially reduces the reported rate | ||
| 1258 | if (thisFrame % SCRIPT_FPS == 0) { | ||
| 1259 | qCDebug(scriptengine) << | ||
| 1260 | "Frame:" << thisFrame << | ||
| 1261 | "Slept (us):" << std::chrono::duration_cast<std::chrono::microseconds>(actuallySleptUntil - beforeSleep).count() << | ||
| 1262 | "Avg Updates (us):" << averageUpdate.count() << | ||
| 1263 | "FPS:" << fps; | ||
| 1264 | } | ||
| 1265 | } | ||
| 1266 | } | ||
| 1267 | #endif | ||
| 1268 | if (_isFinished) { | ||
| 1269 | break; | ||
| 1270 | } | ||
| 1271 | |||
| 1272 | // Only call this if we didn't processEvents as part of waiting for next frame | ||
| 1273 | if (!processedEvents) { | ||
| 1274 | PROFILE_RANGE(script, "processEvents"); | ||
| 1275 | QCoreApplication::processEvents(); | ||
| 1276 | } | ||
| 1277 | |||
| 1278 | if (_isFinished) { | ||
| 1279 | break; | ||
| 1280 | } | ||
| 1281 | |||
| 1282 | if (!_isFinished && entityScriptingInterface->getEntityPacketSender()->serversExist()) { | ||
| 1283 | // release the queue of edit entity messages. | ||
| 1284 | entityScriptingInterface->getEntityPacketSender()->releaseQueuedMessages(); | ||
| 1285 | |||
| 1286 | // since we're in non-threaded mode, call process so that the packets are sent | ||
| 1287 | if (!entityScriptingInterface->getEntityPacketSender()->isThreaded()) { | ||
| 1288 | entityScriptingInterface->getEntityPacketSender()->process(); | ||
| 1289 | } | ||
| 1290 | } | ||
| 1291 | |||
| 1292 | qint64 now = usecTimestampNow(); | ||
| 1293 | |||
| 1294 | // we check for 'now' in the past in case people set their clock back | ||
| 1295 | if (_emitScriptUpdates() && _lastUpdate < now) { | ||
| 1296 | float deltaTime = (float) (now - _lastUpdate) / (float) USECS_PER_SECOND; | ||
| 1297 | if (!_isFinished) { | ||
| 1298 | auto preUpdate = clock::now(); | ||
| 1299 | { | ||
| 1300 | PROFILE_RANGE(script, "ScriptUpdate"); | ||
| 1301 | emit update(deltaTime); | ||
| 1302 | } | ||
| 1303 | auto postUpdate = clock::now(); | ||
| 1304 | auto elapsed = (postUpdate - preUpdate); | ||
| 1305 | totalUpdates += std::chrono::duration_cast<std::chrono::microseconds>(elapsed); | ||
| 1306 | } | ||
| 1307 | } | ||
| 1308 | _lastUpdate = now; | ||
| 1309 | |||
| 1310 | // only clear exceptions if we are not in the middle of evaluating | ||
| 1311 | if (!isEvaluating() && hasUncaughtException()) { | ||
| 1312 | qCWarning(scriptengine) << __FUNCTION__ << "---------- UNCAUGHT EXCEPTION --------"; | ||
| 1313 | qCWarning(scriptengine) << "runInThread" << uncaughtException().toString(); | ||
| 1314 | emit unhandledException(cloneUncaughtException(__FUNCTION__)); | ||
| 1315 | clearExceptions(); | ||
| 1316 | } | ||
| 1317 | } | ||
| 1318 | scriptInfoMessage("Script Engine stopping:" + getFilename()); | ||
| 1319 | |||
| 1320 | stopAllTimers(); // make sure all our timers are stopped if the script is ending | ||
| 1321 | emit scriptEnding(); | ||
| 1322 | |||
| 1323 | if (entityScriptingInterface->getEntityPacketSender()->serversExist()) { | ||
| 1324 | // release the queue of edit entity messages. | ||
| 1325 | entityScriptingInterface->getEntityPacketSender()->releaseQueuedMessages(); | ||
| 1326 | |||
| 1327 | // since we're in non-threaded mode, call process so that the packets are sent | ||
| 1328 | if (!entityScriptingInterface->getEntityPacketSender()->isThreaded()) { | ||
| 1329 | // wait here till the edit packet sender is completely done sending | ||
| 1330 | while (entityScriptingInterface->getEntityPacketSender()->hasPacketsToSend()) { | ||
| 1331 | entityScriptingInterface->getEntityPacketSender()->process(); | ||
| 1332 | QCoreApplication::processEvents(); | ||
| 1333 | } | ||
| 1334 | } else { | ||
| 1335 | // FIXME - do we need to have a similar "wait here" loop for non-threaded packet senders? | ||
| 1336 | } | ||
| 1337 | } | ||
| 1338 | |||
| 1339 | emit finished(_fileNameString, qSharedPointerCast<ScriptEngine>(sharedFromThis())); | ||
| 1340 | |||
| 1341 | // Don't leave our local-file-access flag laying around, reset it to false when the scriptengine | ||
| 1342 | // thread is finished | ||
| 1343 | hifi::scripting::setLocalAccessSafeThread(false); | ||
| 1344 | _isRunning = false; | ||
| 1345 | emit runningStateChanged(); | ||
| 1346 | emit doneRunning(); | ||
| 1347 | } | ||
| 1348 | |||
| 1349 | // NOTE: This is private because it must be called on the same thread that created the timers, which is why | ||
| 1350 | // we want to only call it in our own run "shutdown" processing. | ||
| 1351 | void ScriptEngine::stopAllTimers() { | ||
| 1352 | QMutableHashIterator<QTimer*, CallbackData> i(_timerFunctionMap); | ||
| 1353 | int j {0}; | ||
| 1354 | while (i.hasNext()) { | ||
| 1355 | i.next(); | ||
| 1356 | QTimer* timer = i.key(); | ||
| 1357 | qCDebug(scriptengine) << getFilename() << "stopAllTimers[" << j++ << "]"; | ||
| 1358 | stopTimer(timer); | ||
| 1359 | } | ||
| 1360 | } | ||
| 1361 | |||
| 1362 | void ScriptEngine::stopAllTimersForEntityScript(const EntityItemID& entityID) { | ||
| 1363 | // We could maintain a separate map of entityID => QTimer, but someone will have to prove to me that it's worth the complexity. -HRS | ||
| 1364 | QVector<QTimer*> toDelete; | ||
| 1365 | QMutableHashIterator<QTimer*, CallbackData> i(_timerFunctionMap); | ||
| 1366 | while (i.hasNext()) { | ||
| 1367 | i.next(); | ||
| 1368 | if (i.value().definingEntityIdentifier != entityID) { | ||
| 1369 | continue; | ||
| 1370 | } | ||
| 1371 | QTimer* timer = i.key(); | ||
| 1372 | toDelete << timer; // don't delete while we're iterating. save it. | ||
| 1373 | } | ||
| 1374 | for (auto timer:toDelete) { // now reap 'em | ||
| 1375 | stopTimer(timer); | ||
| 1376 | } | ||
| 1377 | |||
| 1378 | } | ||
| 1379 | |||
| 1380 | void ScriptEngine::stop(bool marshal) { | ||
| 1381 | _isStopping = true; // this can be done on any thread | ||
| 1382 | |||
| 1383 | if (marshal) { | ||
| 1384 | QMetaObject::invokeMethod(this, "stop"); | ||
| 1385 | return; | ||
| 1386 | } | ||
| 1387 | if (!_isFinished) { | ||
| 1388 | _isFinished = true; | ||
| 1389 | emit runningStateChanged(); | ||
| 1390 | } | ||
| 1391 | } | ||
| 1392 | |||
| 1393 | // Other threads can invoke this through invokeMethod, which causes the callback to be asynchronously executed in this script's thread. | ||
| 1394 | void ScriptEngine::callAnimationStateHandler(QScriptValue callback, AnimVariantMap parameters, QStringList names, bool useNames, AnimVariantResultHandler resultHandler) { | ||
| 1395 | if (QThread::currentThread() != thread()) { | ||
| 1396 | #ifdef THREAD_DEBUGGING | ||
| 1397 | qCDebug(scriptengine) << "*** WARNING *** ScriptEngine::callAnimationStateHandler() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] name:" << name; | ||
| 1398 | #endif | ||
| 1399 | QMetaObject::invokeMethod(this, "callAnimationStateHandler", | ||
| 1400 | Q_ARG(QScriptValue, callback), | ||
| 1401 | Q_ARG(AnimVariantMap, parameters), | ||
| 1402 | Q_ARG(QStringList, names), | ||
| 1403 | Q_ARG(bool, useNames), | ||
| 1404 | Q_ARG(AnimVariantResultHandler, resultHandler)); | ||
| 1405 | return; | ||
| 1406 | } | ||
| 1407 | QScriptValue javascriptParameters = parameters.animVariantMapToScriptValue(this, names, useNames); | ||
| 1408 | QScriptValueList callingArguments; | ||
| 1409 | callingArguments << javascriptParameters; | ||
| 1410 | assert(currentEntityIdentifier.isInvalidID()); // No animation state handlers from entity scripts. | ||
| 1411 | QScriptValue result = callback.call(QScriptValue(), callingArguments); | ||
| 1412 | |||
| 1413 | // validate result from callback function. | ||
| 1414 | if (result.isValid() && result.isObject()) { | ||
| 1415 | resultHandler(result); | ||
| 1416 | } else { | ||
| 1417 | qCWarning(scriptengine) << "ScriptEngine::callAnimationStateHandler invalid return argument from callback, expected an object"; | ||
| 1418 | } | ||
| 1419 | } | ||
| 1420 | |||
| 1421 | void ScriptEngine::updateMemoryCost(const qint64& deltaSize) { | ||
| 1422 | if (deltaSize > 0) { | ||
| 1423 | // We've patched qt to fix https://highfidelity.atlassian.net/browse/BUGZ-46 on mac and windows only. | ||
| 1424 | #if defined(Q_OS_WIN) || defined(Q_OS_MAC) | ||
| 1425 | reportAdditionalMemoryCost(deltaSize); | ||
| 1426 | #endif | ||
| 1427 | } | ||
| 1428 | } | ||
| 1429 | |||
| 1430 | void ScriptEngine::timerFired() { | ||
| 1431 | { | ||
| 1432 | QSharedPointer<ScriptEngines> scriptEngines(_scriptEngines); | ||
| 1433 | if (!scriptEngines || scriptEngines->isStopped()) { | ||
| 1434 | scriptWarningMessage("Script.timerFired() while shutting down is ignored... parent script:" + getFilename()); | ||
| 1435 | return; // bail early | ||
| 1436 | } | ||
| 1437 | } | ||
| 1438 | |||
| 1439 | QTimer* callingTimer = reinterpret_cast<QTimer*>(sender()); | ||
| 1440 | CallbackData timerData = _timerFunctionMap.value(callingTimer); | ||
| 1441 | |||
| 1442 | if (!callingTimer->isActive()) { | ||
| 1443 | // this timer is done, we can kill it | ||
| 1444 | _timerFunctionMap.remove(callingTimer); | ||
| 1445 | delete callingTimer; | ||
| 1446 | } | ||
| 1447 | |||
| 1448 | // call the associated JS function, if it exists | ||
| 1449 | if (timerData.function.isValid()) { | ||
| 1450 | PROFILE_RANGE(script, __FUNCTION__); | ||
| 1451 | auto preTimer = p_high_resolution_clock::now(); | ||
| 1452 | callWithEnvironment(timerData.definingEntityIdentifier, timerData.definingSandboxURL, timerData.function, timerData.function, QScriptValueList()); | ||
| 1453 | auto postTimer = p_high_resolution_clock::now(); | ||
| 1454 | auto elapsed = (postTimer - preTimer); | ||
| 1455 | _totalTimerExecution += std::chrono::duration_cast<std::chrono::microseconds>(elapsed); | ||
| 1456 | } else { | ||
| 1457 | qCWarning(scriptengine) << "timerFired -- invalid function" << timerData.function.toVariant().toString(); | ||
| 1458 | } | ||
| 1459 | } | ||
| 1460 | |||
| 1461 | QObject* ScriptEngine::setupTimerWithInterval(const QScriptValue& function, int intervalMS, bool isSingleShot) { | ||
| 1462 | // create the timer, add it to the map, and start it | ||
| 1463 | QTimer* newTimer = new QTimer(this); | ||
| 1464 | newTimer->setSingleShot(isSingleShot); | ||
| 1465 | |||
| 1466 | // The default timer type is not very accurate below about 200ms http://doc.qt.io/qt-5/qt.html#TimerType-enum | ||
| 1467 | static const int MIN_TIMEOUT_FOR_COARSE_TIMER = 200; | ||
| 1468 | if (intervalMS < MIN_TIMEOUT_FOR_COARSE_TIMER) { | ||
| 1469 | newTimer->setTimerType(Qt::PreciseTimer); | ||
| 1470 | } | ||
| 1471 | |||
| 1472 | connect(newTimer, &QTimer::timeout, this, &ScriptEngine::timerFired); | ||
| 1473 | |||
| 1474 | // make sure the timer stops when the script does | ||
| 1475 | connect(this, &ScriptEngine::scriptEnding, newTimer, &QTimer::stop); | ||
| 1476 | |||
| 1477 | |||
| 1478 | CallbackData timerData = { function, currentEntityIdentifier, currentSandboxURL }; | ||
| 1479 | _timerFunctionMap.insert(newTimer, timerData); | ||
| 1480 | |||
| 1481 | newTimer->start(intervalMS); | ||
| 1482 | return newTimer; | ||
| 1483 | } | ||
| 1484 | |||
| 1485 | QObject* ScriptEngine::setInterval(const QScriptValue& function, int intervalMS) { | ||
| 1486 | QSharedPointer<ScriptEngines> scriptEngines(_scriptEngines); | ||
| 1487 | if (!scriptEngines || scriptEngines->isStopped()) { | ||
| 1488 | scriptWarningMessage("Script.setInterval() while shutting down is ignored... parent script:" + getFilename()); | ||
| 1489 | return NULL; // bail early | ||
| 1490 | } | ||
| 1491 | |||
| 1492 | return setupTimerWithInterval(function, intervalMS, false); | ||
| 1493 | } | ||
| 1494 | |||
| 1495 | QObject* ScriptEngine::setTimeout(const QScriptValue& function, int timeoutMS) { | ||
| 1496 | QSharedPointer<ScriptEngines> scriptEngines(_scriptEngines); | ||
| 1497 | if (!scriptEngines || scriptEngines->isStopped()) { | ||
| 1498 | scriptWarningMessage("Script.setTimeout() while shutting down is ignored... parent script:" + getFilename()); | ||
| 1499 | return NULL; // bail early | ||
| 1500 | } | ||
| 1501 | |||
| 1502 | return setupTimerWithInterval(function, timeoutMS, true); | ||
| 1503 | } | ||
| 1504 | |||
| 1505 | void ScriptEngine::stopTimer(QTimer *timer) { | ||
| 1506 | if (_timerFunctionMap.contains(timer)) { | ||
| 1507 | timer->stop(); | ||
| 1508 | _timerFunctionMap.remove(timer); | ||
| 1509 | delete timer; | ||
| 1510 | } else { | ||
| 1511 | qCDebug(scriptengine) << "stopTimer -- not in _timerFunctionMap" << timer; | ||
| 1512 | } | ||
| 1513 | } | ||
| 1514 | |||
| 1515 | QUrl ScriptEngine::resolvePath(const QString& include) const { | ||
| 1516 | QUrl url(include); | ||
| 1517 | // first lets check to see if it's already a full URL -- or a Windows path like "c:/" | ||
| 1518 | if (include.startsWith("/") || url.scheme().length() == 1) { | ||
| 1519 | url = QUrl::fromLocalFile(include); | ||
| 1520 | } | ||
| 1521 | if (!url.isRelative()) { | ||
| 1522 | return expandScriptUrl(url); | ||
| 1523 | } | ||
| 1524 | |||
| 1525 | // we apparently weren't a fully qualified url, so, let's assume we're relative | ||
| 1526 | // to the first absolute URL in the JS scope chain | ||
| 1527 | QUrl parentURL; | ||
| 1528 | auto context = currentContext(); | ||
| 1529 | do { | ||
| 1530 | QScriptContextInfo contextInfo { context }; | ||
| 1531 | parentURL = QUrl(contextInfo.fileName()); | ||
| 1532 | context = context->parentContext(); | ||
| 1533 | } while (parentURL.isRelative() && context); | ||
| 1534 | |||
| 1535 | if (parentURL.isRelative()) { | ||
| 1536 | // fallback to the "include" parent (if defined, this will already be absolute) | ||
| 1537 | parentURL = QUrl(_parentURL); | ||
| 1538 | } | ||
| 1539 | |||
| 1540 | if (parentURL.isRelative()) { | ||
| 1541 | // fallback to the original script engine URL | ||
| 1542 | parentURL = QUrl(_fileNameString); | ||
| 1543 | |||
| 1544 | // if still relative and path-like, then this is probably a local file... | ||
| 1545 | if (parentURL.isRelative() && url.path().contains("/")) { | ||
| 1546 | parentURL = QUrl::fromLocalFile(_fileNameString); | ||
| 1547 | } | ||
| 1548 | } | ||
| 1549 | |||
| 1550 | // at this point we should have a legitimate fully qualified URL for our parent | ||
| 1551 | url = expandScriptUrl(parentURL.resolved(url)); | ||
| 1552 | return url; | ||
| 1553 | } | ||
| 1554 | |||
| 1555 | QUrl ScriptEngine::resourcesPath() const { | ||
| 1556 | return QUrl(PathUtils::resourcesUrl()); | ||
| 1557 | } | ||
| 1558 | |||
| 1559 | void ScriptEngine::print(const QString& message) { | ||
| 1560 | emit printedMessage(message, getFilename()); | ||
| 1561 | } | ||
| 1562 | |||
| 1563 | |||
| 1564 | void ScriptEngine::beginProfileRange(const QString& label) const { | ||
| 1565 | PROFILE_SYNC_BEGIN(script, label.toStdString().c_str(), label.toStdString().c_str()); | ||
| 1566 | } | ||
| 1567 | |||
| 1568 | void ScriptEngine::endProfileRange(const QString& label) const { | ||
| 1569 | PROFILE_SYNC_END(script, label.toStdString().c_str(), label.toStdString().c_str()); | ||
| 1570 | } | ||
| 1571 | |||
| 1572 | // Script.require.resolve -- like resolvePath, but performs more validation and throws exceptions on invalid module identifiers (for consistency with Node.js) | ||
| 1573 | QString ScriptEngine::_requireResolve(const QString& moduleId, const QString& relativeTo) { | ||
| 1574 | if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { | ||
| 1575 | return QString(); | ||
| 1576 | } | ||
| 1577 | QUrl defaultScriptsLoc = PathUtils::defaultScriptsLocation(); | ||
| 1578 | QUrl url(moduleId); | ||
| 1579 | |||
| 1580 | auto displayId = moduleId; | ||
| 1581 | if (displayId.length() > MAX_DEBUG_VALUE_LENGTH) { | ||
| 1582 | displayId = displayId.mid(0, MAX_DEBUG_VALUE_LENGTH) + "..."; | ||
| 1583 | } | ||
| 1584 | auto message = QString("Cannot find module '%1' (%2)").arg(displayId); | ||
| 1585 | |||
| 1586 | auto throwResolveError = [&](const QScriptValue& error) -> QString { | ||
| 1587 | raiseException(error); | ||
| 1588 | maybeEmitUncaughtException("require.resolve"); | ||
| 1589 | return QString(); | ||
| 1590 | }; | ||
| 1591 | |||
| 1592 | // de-fuzz the input a little by restricting to rational sizes | ||
| 1593 | auto idLength = url.toString().length(); | ||
| 1594 | if (idLength < 1 || idLength > MAX_MODULE_ID_LENGTH) { | ||
| 1595 | auto details = QString("rejecting invalid module id size (%1 chars [1,%2])") | ||
| 1596 | .arg(idLength).arg(MAX_MODULE_ID_LENGTH); | ||
| 1597 | return throwResolveError(makeError(message.arg(details), "RangeError")); | ||
| 1598 | } | ||
| 1599 | |||
| 1600 | // this regex matches: absolute, dotted or path-like URLs | ||
| 1601 | // (ie: the kind of stuff ScriptEngine::resolvePath already handles) | ||
| 1602 | QRegularExpression qualified ("^\\w+:|^/|^[.]{1,2}(/|$)"); | ||
| 1603 | |||
| 1604 | // this is for module.require (which is a bound version of require that's always relative to the module path) | ||
| 1605 | if (!relativeTo.isEmpty()) { | ||
| 1606 | url = QUrl(relativeTo).resolved(moduleId); | ||
| 1607 | url = resolvePath(url.toString()); | ||
| 1608 | } else if (qualified.match(moduleId).hasMatch()) { | ||
| 1609 | url = resolvePath(moduleId); | ||
| 1610 | } else { | ||
| 1611 | // check if the moduleId refers to a "system" module | ||
| 1612 | QString systemPath = defaultScriptsLoc.path(); | ||
| 1613 | QString systemModulePath = QString("%1/modules/%2.js").arg(systemPath).arg(moduleId); | ||
| 1614 | url = defaultScriptsLoc; | ||
| 1615 | url.setPath(systemModulePath); | ||
| 1616 | if (!QFileInfo(url.toLocalFile()).isFile()) { | ||
| 1617 | if (!moduleId.contains("./")) { | ||
| 1618 | // the user might be trying to refer to a relative file without anchoring it | ||
| 1619 | // let's do them a favor and test for that case -- offering specific advice if detected | ||
| 1620 | auto unanchoredUrl = resolvePath("./" + moduleId); | ||
| 1621 | if (QFileInfo(unanchoredUrl.toLocalFile()).isFile()) { | ||
| 1622 | auto msg = QString("relative module ids must be anchored; use './%1' instead") | ||
| 1623 | .arg(moduleId); | ||
| 1624 | return throwResolveError(makeError(message.arg(msg))); | ||
| 1625 | } | ||
| 1626 | } | ||
| 1627 | return throwResolveError(makeError(message.arg("system module not found"))); | ||
| 1628 | } | ||
| 1629 | } | ||
| 1630 | |||
| 1631 | if (url.isRelative()) { | ||
| 1632 | return throwResolveError(makeError(message.arg("could not resolve module id"))); | ||
| 1633 | } | ||
| 1634 | |||
| 1635 | // if it looks like a local file, verify that it's an allowed path and really a file | ||
| 1636 | if (url.isLocalFile()) { | ||
| 1637 | QFileInfo file(url.toLocalFile()); | ||
| 1638 | QUrl canonical = url; | ||
| 1639 | if (file.exists()) { | ||
| 1640 | canonical.setPath(file.canonicalFilePath()); | ||
| 1641 | } | ||
| 1642 | |||
| 1643 | bool disallowOutsideFiles = !PathUtils::defaultScriptsLocation().isParentOf(canonical) && !currentSandboxURL.isLocalFile(); | ||
| 1644 | if (disallowOutsideFiles && !PathUtils::isDescendantOf(canonical, currentSandboxURL)) { | ||
| 1645 | return throwResolveError(makeError(message.arg( | ||
| 1646 | QString("path '%1' outside of origin script '%2' '%3'") | ||
| 1647 | .arg(PathUtils::stripFilename(url)) | ||
| 1648 | .arg(PathUtils::stripFilename(currentSandboxURL)) | ||
| 1649 | .arg(canonical.toString()) | ||
| 1650 | ))); | ||
| 1651 | } | ||
| 1652 | if (!file.exists()) { | ||
| 1653 | return throwResolveError(makeError(message.arg("path does not exist: " + url.toLocalFile()))); | ||
| 1654 | } | ||
| 1655 | if (!file.isFile()) { | ||
| 1656 | return throwResolveError(makeError(message.arg("path is not a file: " + url.toLocalFile()))); | ||
| 1657 | } | ||
| 1658 | } | ||
| 1659 | |||
| 1660 | maybeEmitUncaughtException(__FUNCTION__); | ||
| 1661 | return url.toString(); | ||
| 1662 | } | ||
| 1663 | |||
| 1664 | // retrieves the current parent module from the JS scope chain | ||
| 1665 | QScriptValue ScriptEngine::currentModule() { | ||
| 1666 | if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { | ||
| 1667 | return unboundNullValue(); | ||
| 1668 | } | ||
| 1669 | auto jsRequire = globalObject().property("Script").property("require"); | ||
| 1670 | auto cache = jsRequire.property("cache"); | ||
| 1671 | auto candidate = QScriptValue(); | ||
| 1672 | for (auto c = currentContext(); c && !candidate.isObject(); c = c->parentContext()) { | ||
| 1673 | QScriptContextInfo contextInfo { c }; | ||
| 1674 | candidate = cache.property(contextInfo.fileName()); | ||
| 1675 | } | ||
| 1676 | if (!candidate.isObject()) { | ||
| 1677 | return QScriptValue(); | ||
| 1678 | } | ||
| 1679 | return candidate; | ||
| 1680 | } | ||
| 1681 | |||
| 1682 | // replaces or adds "module" to "parent.children[]" array | ||
| 1683 | // (for consistency with Node.js and userscript cache invalidation without "cache busters") | ||
| 1684 | bool ScriptEngine::registerModuleWithParent(const QScriptValue& module, const QScriptValue& parent) { | ||
| 1685 | auto children = parent.property("children"); | ||
| 1686 | if (children.isArray()) { | ||
| 1687 | auto key = module.property("id"); | ||
| 1688 | auto length = children.property("length").toInt32(); | ||
| 1689 | for (int i = 0; i < length; i++) { | ||
| 1690 | if (children.property(i).property("id").strictlyEquals(key)) { | ||
| 1691 | qCDebug(scriptengine_module) << key.toString() << " updating parent.children[" << i << "] = module"; | ||
| 1692 | children.setProperty(i, module); | ||
| 1693 | return true; | ||
| 1694 | } | ||
| 1695 | } | ||
| 1696 | qCDebug(scriptengine_module) << key.toString() << " appending parent.children[" << length << "] = module"; | ||
| 1697 | children.setProperty(length, module); | ||
| 1698 | return true; | ||
| 1699 | } else if (parent.isValid()) { | ||
| 1700 | qCDebug(scriptengine_module) << "registerModuleWithParent -- unrecognized parent" << parent.toVariant().toString(); | ||
| 1701 | } | ||
| 1702 | return false; | ||
| 1703 | } | ||
| 1704 | |||
| 1705 | // creates a new JS "module" Object with default metadata properties | ||
| 1706 | QScriptValue ScriptEngine::newModule(const QString& modulePath, const QScriptValue& parent) { | ||
| 1707 | auto closure = newObject(); | ||
| 1708 | auto exports = newObject(); | ||
| 1709 | auto module = newObject(); | ||
| 1710 | qCDebug(scriptengine_module) << "newModule" << parent.property("filename").toString(); | ||
| 1711 | |||
| 1712 | closure.setProperty("module", module, READONLY_PROP_FLAGS); | ||
| 1713 | |||
| 1714 | // note: this becomes the "exports" free variable, so should not be set read only | ||
| 1715 | closure.setProperty("exports", exports); | ||
| 1716 | |||
| 1717 | // make the closure available to module instantiation | ||
| 1718 | module.setProperty("__closure__", closure, READONLY_HIDDEN_PROP_FLAGS); | ||
| 1719 | |||
| 1720 | // for consistency with Node.js Module | ||
| 1721 | module.setProperty("id", modulePath, READONLY_PROP_FLAGS); | ||
| 1722 | module.setProperty("filename", modulePath, READONLY_PROP_FLAGS); | ||
| 1723 | module.setProperty("exports", exports); // not readonly | ||
| 1724 | module.setProperty("loaded", false, READONLY_PROP_FLAGS); | ||
| 1725 | module.setProperty("parent", parent, READONLY_PROP_FLAGS); | ||
| 1726 | module.setProperty("children", newArray(), READONLY_PROP_FLAGS); | ||
| 1727 | |||
| 1728 | // module.require is a bound version of require that always resolves relative to that module's path | ||
| 1729 | auto boundRequire = QScriptEngine::evaluate("(function(id) { return Script.require(Script.require.resolve(id, this.filename)); })", "(boundRequire)"); | ||
| 1730 | module.setProperty("require", boundRequire, READONLY_PROP_FLAGS); | ||
| 1731 | |||
| 1732 | return module; | ||
| 1733 | } | ||
| 1734 | |||
| 1735 | // synchronously fetch a module's source code using BatchLoader | ||
| 1736 | QVariantMap ScriptEngine::fetchModuleSource(const QString& modulePath, const bool forceDownload) { | ||
| 1737 | using UrlMap = QMap<QUrl, QString>; | ||
| 1738 | auto scriptCache = DependencyManager::get<ScriptCache>(); | ||
| 1739 | QVariantMap req; | ||
| 1740 | qCDebug(scriptengine_module) << "require.fetchModuleSource: " << QUrl(modulePath).fileName() << QThread::currentThread(); | ||
| 1741 | |||
| 1742 | auto onload = [=, &req](const UrlMap& data, const UrlMap& _status) { | ||
| 1743 | auto url = modulePath; | ||
| 1744 | auto status = _status[url]; | ||
| 1745 | auto contents = data[url]; | ||
| 1746 | if (isStopping()) { | ||
| 1747 | req["status"] = "Stopped"; | ||
| 1748 | req["success"] = false; | ||
| 1749 | } else { | ||
| 1750 | req["url"] = url; | ||
| 1751 | req["status"] = status; | ||
| 1752 | req["success"] = ScriptCache::isSuccessStatus(status); | ||
| 1753 | req["contents"] = contents; | ||
| 1754 | } | ||
| 1755 | }; | ||
| 1756 | |||
| 1757 | if (forceDownload) { | ||
| 1758 | qCDebug(scriptengine_module) << "require.requestScript -- clearing cache for" << modulePath; | ||
| 1759 | scriptCache->deleteScript(modulePath); | ||
| 1760 | } | ||
| 1761 | BatchLoader* loader = new BatchLoader(QList<QUrl>({ modulePath })); | ||
| 1762 | connect(loader, &BatchLoader::finished, this, onload); | ||
| 1763 | connect(this, &QObject::destroyed, loader, &QObject::deleteLater); | ||
| 1764 | // fail faster? (since require() blocks the engine thread while resolving dependencies) | ||
| 1765 | const int MAX_RETRIES = 1; | ||
| 1766 | |||
| 1767 | loader->start(MAX_RETRIES); | ||
| 1768 | |||
| 1769 | if (!loader->isFinished()) { | ||
| 1770 | // This lambda can get called AFTER this local scope has completed. | ||
| 1771 | // This is why we pass smart ptrs to the lambda instead of references to local variables. | ||
| 1772 | auto monitor = std::make_shared<QTimer>(); | ||
| 1773 | auto loop = std::make_shared<QEventLoop>(); | ||
| 1774 | QObject::connect(loader, &BatchLoader::finished, this, [monitor, loop] { | ||
| 1775 | monitor->stop(); | ||
| 1776 | loop->quit(); | ||
| 1777 | }); | ||
| 1778 | |||
| 1779 | // this helps detect the case where stop() is invoked during the download | ||
| 1780 | // but not seen in time to abort processing in onload()... | ||
| 1781 | connect(monitor.get(), &QTimer::timeout, this, [this, loop] { | ||
| 1782 | if (isStopping()) { | ||
| 1783 | loop->exit(-1); | ||
| 1784 | } | ||
| 1785 | }); | ||
| 1786 | monitor->start(500); | ||
| 1787 | loop->exec(); | ||
| 1788 | } | ||
| 1789 | loader->deleteLater(); | ||
| 1790 | return req; | ||
| 1791 | } | ||
| 1792 | |||
| 1793 | // evaluate a pending module object using the fetched source code | ||
| 1794 | QScriptValue ScriptEngine::instantiateModule(const QScriptValue& module, const QString& sourceCode) { | ||
| 1795 | QScriptValue result; | ||
| 1796 | auto modulePath = module.property("filename").toString(); | ||
| 1797 | auto closure = module.property("__closure__"); | ||
| 1798 | |||
| 1799 | qCDebug(scriptengine_module) << QString("require.instantiateModule: %1 / %2 bytes") | ||
| 1800 | .arg(QUrl(modulePath).fileName()).arg(sourceCode.length()); | ||
| 1801 | |||
| 1802 | if (module.property("content-type").toString() == "application/json") { | ||
| 1803 | qCDebug(scriptengine_module) << "... parsing as JSON"; | ||
| 1804 | closure.setProperty("__json", sourceCode); | ||
| 1805 | result = evaluateInClosure(closure, { "module.exports = JSON.parse(__json)", modulePath }); | ||
| 1806 | } else { | ||
| 1807 | // scoped vars for consistency with Node.js | ||
| 1808 | closure.setProperty("require", module.property("require")); | ||
| 1809 | closure.setProperty("__filename", modulePath, READONLY_HIDDEN_PROP_FLAGS); | ||
| 1810 | closure.setProperty("__dirname", QString(modulePath).replace(QRegExp("/[^/]*$"), ""), READONLY_HIDDEN_PROP_FLAGS); | ||
| 1811 | result = evaluateInClosure(closure, { sourceCode, modulePath }); | ||
| 1812 | } | ||
| 1813 | maybeEmitUncaughtException(__FUNCTION__); | ||
| 1814 | return result; | ||
| 1815 | } | ||
| 1816 | |||
| 1817 | // CommonJS/Node.js like require/module support | ||
| 1818 | QScriptValue ScriptEngine::require(const QString& moduleId) { | ||
| 1819 | qCDebug(scriptengine_module) << "ScriptEngine::require(" << moduleId.left(MAX_DEBUG_VALUE_LENGTH) << ")"; | ||
| 1820 | if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { | ||
| 1821 | return unboundNullValue(); | ||
| 1822 | } | ||
| 1823 | |||
| 1824 | auto jsRequire = globalObject().property("Script").property("require"); | ||
| 1825 | auto cacheMeta = jsRequire.data(); | ||
| 1826 | auto cache = jsRequire.property("cache"); | ||
| 1827 | auto parent = currentModule(); | ||
| 1828 | |||
| 1829 | auto throwModuleError = [&](const QString& modulePath, const QScriptValue& error) { | ||
| 1830 | cache.setProperty(modulePath, nullValue()); | ||
| 1831 | if (!error.isNull()) { | ||
| 1832 | #ifdef DEBUG_JS_MODULES | ||
| 1833 | qCWarning(scriptengine_module) << "throwing module error:" << error.toString() << modulePath << error.property("stack").toString(); | ||
| 1834 | #endif | ||
| 1835 | raiseException(error); | ||
| 1836 | } | ||
| 1837 | maybeEmitUncaughtException("module"); | ||
| 1838 | return unboundNullValue(); | ||
| 1839 | }; | ||
| 1840 | |||
| 1841 | // start by resolving the moduleId into a fully-qualified path/URL | ||
| 1842 | QString modulePath = _requireResolve(moduleId); | ||
| 1843 | if (modulePath.isNull() || hasUncaughtException()) { | ||
| 1844 | // the resolver already threw an exception -- bail early | ||
| 1845 | maybeEmitUncaughtException(__FUNCTION__); | ||
| 1846 | return unboundNullValue(); | ||
| 1847 | } | ||
| 1848 | |||
| 1849 | // check the resolved path against the cache | ||
| 1850 | auto module = cache.property(modulePath); | ||
| 1851 | |||
| 1852 | // modules get cached in `Script.require.cache` and (similar to Node.js) users can access it | ||
| 1853 | // to inspect particular entries and invalidate them by deleting the key: | ||
| 1854 | // `delete Script.require.cache[Script.require.resolve(moduleId)];` | ||
| 1855 | |||
| 1856 | // Check to see if we should invalidate the cache based on a user setting. | ||
| 1857 | Setting::Handle<bool> getCachebustSetting {"cachebustScriptRequire", false }; | ||
| 1858 | |||
| 1859 | // cacheMeta is just used right now to tell deleted keys apart from undefined ones | ||
| 1860 | bool invalidateCache = getCachebustSetting.get() || (module.isUndefined() && cacheMeta.property(moduleId).isValid()); | ||
| 1861 | |||
| 1862 | // reset the cacheMeta record so invalidation won't apply next time, even if the module fails to load | ||
| 1863 | cacheMeta.setProperty(modulePath, QScriptValue()); | ||
| 1864 | |||
| 1865 | auto exports = module.property("exports"); | ||
| 1866 | if (!invalidateCache && exports.isObject()) { | ||
| 1867 | // we have found a cached module -- just need to possibly register it with current parent | ||
| 1868 | qCDebug(scriptengine_module) << QString("require - using cached module for '%1' (loaded: %2)") | ||
| 1869 | .arg(moduleId).arg(module.property("loaded").toString()); | ||
| 1870 | registerModuleWithParent(module, parent); | ||
| 1871 | maybeEmitUncaughtException("cached module"); | ||
| 1872 | return exports; | ||
| 1873 | } | ||
| 1874 | |||
| 1875 | // bootstrap / register new empty module | ||
| 1876 | module = newModule(modulePath, parent); | ||
| 1877 | registerModuleWithParent(module, parent); | ||
| 1878 | |||
| 1879 | // add it to the cache (this is done early so any cyclic dependencies pick up) | ||
| 1880 | cache.setProperty(modulePath, module); | ||
| 1881 | |||
| 1882 | // download the module source | ||
| 1883 | auto req = fetchModuleSource(modulePath, invalidateCache); | ||
| 1884 | |||
| 1885 | if (!req.contains("success") || !req["success"].toBool()) { | ||
| 1886 | auto error = QString("error retrieving script (%1)").arg(req["status"].toString()); | ||
| 1887 | return throwModuleError(modulePath, error); | ||
| 1888 | } | ||
| 1889 | |||
| 1890 | #if DEBUG_JS_MODULES | ||
| 1891 | qCDebug(scriptengine_module) << "require.loaded: " << | ||
| 1892 | QUrl(req["url"].toString()).fileName() << req["status"].toString(); | ||
| 1893 | #endif | ||
| 1894 | |||
| 1895 | auto sourceCode = req["contents"].toString(); | ||
| 1896 | |||
| 1897 | if (QUrl(modulePath).fileName().endsWith(".json", Qt::CaseInsensitive)) { | ||
| 1898 | module.setProperty("content-type", "application/json"); | ||
| 1899 | } else { | ||
| 1900 | module.setProperty("content-type", "application/javascript"); | ||
| 1901 | } | ||
| 1902 | |||
| 1903 | // evaluate the module | ||
| 1904 | auto result = instantiateModule(module, sourceCode); | ||
| 1905 | |||
| 1906 | if (result.isError() && !result.strictlyEquals(module.property("exports"))) { | ||
| 1907 | qCWarning(scriptengine_module) << "-- result.isError --" << result.toString(); | ||
| 1908 | return throwModuleError(modulePath, result); | ||
| 1909 | } | ||
| 1910 | |||
| 1911 | // mark as fully-loaded | ||
| 1912 | module.setProperty("loaded", true, READONLY_PROP_FLAGS); | ||
| 1913 | |||
| 1914 | // set up a new reference point for detecting cache key deletion | ||
| 1915 | cacheMeta.setProperty(modulePath, module); | ||
| 1916 | |||
| 1917 | qCDebug(scriptengine_module) << "//ScriptEngine::require(" << moduleId << ")"; | ||
| 1918 | |||
| 1919 | maybeEmitUncaughtException(__FUNCTION__); | ||
| 1920 | return module.property("exports"); | ||
| 1921 | } | ||
| 1922 | |||
| 1923 | // If a callback is specified, the included files will be loaded asynchronously and the callback will be called | ||
| 1924 | // when all of the files have finished loading. | ||
| 1925 | // If no callback is specified, the included files will be loaded synchronously and will block execution until | ||
| 1926 | // all of the files have finished loading. | ||
| 1927 | void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callback) { | ||
| 1928 | if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { | ||
| 1929 | return; | ||
| 1930 | } | ||
| 1931 | QSharedPointer<ScriptEngines> scriptEngines(_scriptEngines); | ||
| 1932 | if (!scriptEngines || scriptEngines->isStopped()) { | ||
| 1933 | scriptWarningMessage("Script.include() while shutting down is ignored... includeFiles:" | ||
| 1934 | + includeFiles.join(",") + "parent script:" + getFilename()); | ||
| 1935 | return; // bail early | ||
| 1936 | } | ||
| 1937 | QList<QUrl> urls; | ||
| 1938 | |||
| 1939 | for (QString includeFile : includeFiles) { | ||
| 1940 | QString file = DependencyManager::get<ResourceManager>()->normalizeURL(includeFile); | ||
| 1941 | QUrl thisURL; | ||
| 1942 | bool isStandardLibrary = false; | ||
| 1943 | if (file.startsWith("/~/")) { | ||
| 1944 | thisURL = expandScriptUrl(QUrl::fromLocalFile(expandScriptPath(file))); | ||
| 1945 | QUrl defaultScriptsLoc = PathUtils::defaultScriptsLocation(); | ||
| 1946 | if (!defaultScriptsLoc.isParentOf(thisURL)) { | ||
| 1947 | scriptWarningMessage("Script.include() -- skipping" + file + "-- outside of standard libraries"); | ||
| 1948 | continue; | ||
| 1949 | } | ||
| 1950 | isStandardLibrary = true; | ||
| 1951 | } else { | ||
| 1952 | thisURL = resolvePath(file); | ||
| 1953 | } | ||
| 1954 | |||
| 1955 | bool disallowOutsideFiles = thisURL.isLocalFile() && !isStandardLibrary && !currentSandboxURL.isLocalFile(); | ||
| 1956 | if (disallowOutsideFiles && !PathUtils::isDescendantOf(thisURL, currentSandboxURL)) { | ||
| 1957 | scriptWarningMessage("Script.include() ignoring file path" + thisURL.toString() | ||
| 1958 | + "outside of original entity script" + currentSandboxURL.toString()); | ||
| 1959 | } else { | ||
| 1960 | // We could also check here for CORS, but we don't yet. | ||
| 1961 | // It turns out that QUrl.resolve will not change hosts and copy authority, so we don't need to check that here. | ||
| 1962 | urls.append(thisURL); | ||
| 1963 | } | ||
| 1964 | } | ||
| 1965 | |||
| 1966 | // If there are no URLs left to download, don't bother attempting to download anything and return early | ||
| 1967 | if (urls.size() == 0) { | ||
| 1968 | return; | ||
| 1969 | } | ||
| 1970 | |||
| 1971 | BatchLoader* loader = new BatchLoader(urls); | ||
| 1972 | EntityItemID capturedEntityIdentifier = currentEntityIdentifier; | ||
| 1973 | QUrl capturedSandboxURL = currentSandboxURL; | ||
| 1974 | |||
| 1975 | auto evaluateScripts = [=](const QMap<QUrl, QString>& data, const QMap<QUrl, QString>& status) { | ||
| 1976 | auto parentURL = _parentURL; | ||
| 1977 | for (QUrl url : urls) { | ||
| 1978 | QString contents = data[url]; | ||
| 1979 | if (contents.isNull()) { | ||
| 1980 | scriptErrorMessage("Error loading file (" + status[url] +"): " + url.toString()); | ||
| 1981 | } else { | ||
| 1982 | std::lock_guard<std::recursive_mutex> lock(_lock); | ||
| 1983 | if (!_includedURLs.contains(url)) { | ||
| 1984 | _includedURLs << url; | ||
| 1985 | // Set the parent url so that path resolution will be relative | ||
| 1986 | // to this script's url during its initial evaluation | ||
| 1987 | _parentURL = url.toString(); | ||
| 1988 | auto operation = [&]() { | ||
| 1989 | evaluate(contents, url.toString()); | ||
| 1990 | }; | ||
| 1991 | |||
| 1992 | doWithEnvironment(capturedEntityIdentifier, capturedSandboxURL, operation); | ||
| 1993 | if (hasUncaughtException()) { | ||
| 1994 | emit unhandledException(cloneUncaughtException("evaluateInclude")); | ||
| 1995 | clearExceptions(); | ||
| 1996 | } | ||
| 1997 | } else { | ||
| 1998 | scriptPrintedMessage("Script.include() skipping evaluation of previously included url:" + url.toString()); | ||
| 1999 | } | ||
| 2000 | } | ||
| 2001 | } | ||
| 2002 | _parentURL = parentURL; | ||
| 2003 | |||
| 2004 | if (callback.isFunction()) { | ||
| 2005 | callWithEnvironment(capturedEntityIdentifier, capturedSandboxURL, QScriptValue(callback), QScriptValue(), QScriptValueList()); | ||
| 2006 | } | ||
| 2007 | |||
| 2008 | loader->deleteLater(); | ||
| 2009 | }; | ||
| 2010 | |||
| 2011 | connect(loader, &BatchLoader::finished, this, evaluateScripts); | ||
| 2012 | |||
| 2013 | // If we are destroyed before the loader completes, make sure to clean it up | ||
| 2014 | connect(this, &QObject::destroyed, loader, &QObject::deleteLater); | ||
| 2015 | |||
| 2016 | loader->start(processLevelMaxRetries); | ||
| 2017 | |||
| 2018 | if (!callback.isFunction() && !loader->isFinished()) { | ||
| 2019 | QEventLoop loop; | ||
| 2020 | QObject::connect(loader, &BatchLoader::finished, &loop, &QEventLoop::quit); | ||
| 2021 | loop.exec(); | ||
| 2022 | } | ||
| 2023 | } | ||
| 2024 | |||
| 2025 | void ScriptEngine::include(const QString& includeFile, QScriptValue callback) { | ||
| 2026 | QSharedPointer<ScriptEngines> scriptEngines(_scriptEngines); | ||
| 2027 | if (!scriptEngines || scriptEngines->isStopped()) { | ||
| 2028 | scriptWarningMessage("Script.include() while shutting down is ignored... includeFile:" | ||
| 2029 | + includeFile + "parent script:" + getFilename()); | ||
| 2030 | return; // bail early | ||
| 2031 | } | ||
| 2032 | |||
| 2033 | QStringList urls; | ||
| 2034 | urls.append(includeFile); | ||
| 2035 | include(urls, callback); | ||
| 2036 | } | ||
| 2037 | |||
| 2038 | // NOTE: The load() command is similar to the include() command except that it loads the script | ||
| 2039 | // as a stand-alone script. To accomplish this, the ScriptEngine class just emits a signal which | ||
| 2040 | // the Application or other context will connect to in order to know to actually load the script | ||
| 2041 | void ScriptEngine::load(const QString& loadFile) { | ||
| 2042 | if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { | ||
| 2043 | return; | ||
| 2044 | } | ||
| 2045 | QSharedPointer<ScriptEngines> scriptEngines(_scriptEngines); | ||
| 2046 | if (!scriptEngines || scriptEngines->isStopped()) { | ||
| 2047 | scriptWarningMessage("Script.load() while shutting down is ignored... loadFile:" | ||
| 2048 | + loadFile + "parent script:" + getFilename()); | ||
| 2049 | return; // bail early | ||
| 2050 | } | ||
| 2051 | if (!currentEntityIdentifier.isInvalidID()) { | ||
| 2052 | scriptWarningMessage("Script.load() from entity script is ignored... loadFile:" | ||
| 2053 | + loadFile + "parent script:" + getFilename() + "entity: " + currentEntityIdentifier.toString()); | ||
| 2054 | return; // bail early | ||
| 2055 | } | ||
| 2056 | |||
| 2057 | QUrl url = resolvePath(loadFile); | ||
| 2058 | if (_isReloading) { | ||
| 2059 | auto scriptCache = DependencyManager::get<ScriptCache>(); | ||
| 2060 | scriptCache->deleteScript(url.toString()); | ||
| 2061 | emit reloadScript(url.toString(), false); | ||
| 2062 | } else { | ||
| 2063 | emit loadScript(url.toString(), false); | ||
| 2064 | } | ||
| 2065 | } | ||
| 2066 | |||
| 2067 | // Look up the handler associated with eventName and entityID. If found, evalute the argGenerator thunk and call the handler with those args | ||
| 2068 | void ScriptEngine::forwardHandlerCall(const EntityItemID& entityID, const QString& eventName, QScriptValueList eventHandlerArgs) { | ||
| 2069 | if (QThread::currentThread() != thread()) { | ||
| 2070 | qCDebug(scriptengine) << "*** ERROR *** ScriptEngine::forwardHandlerCall() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "]"; | ||
| 2071 | assert(false); | ||
| 2072 | return ; | ||
| 2073 | } | ||
| 2074 | if (!_registeredHandlers.contains(entityID)) { | ||
| 2075 | return; | ||
| 2076 | } | ||
| 2077 | const RegisteredEventHandlers& handlersOnEntity = _registeredHandlers[entityID]; | ||
| 2078 | if (!handlersOnEntity.contains(eventName)) { | ||
| 2079 | return; | ||
| 2080 | } | ||
| 2081 | CallbackList handlersForEvent = handlersOnEntity[eventName]; | ||
| 2082 | if (!handlersForEvent.isEmpty()) { | ||
| 2083 | for (int i = 0; i < handlersForEvent.count(); ++i) { | ||
| 2084 | // handlersForEvent[i] can contain many handlers that may have each been added by different interface or entity scripts, | ||
| 2085 | // and the entity scripts may be for entities other than the one this is a handler for. | ||
| 2086 | // Fortunately, the definingEntityIdentifier captured the entity script id (if any) when the handler was added. | ||
| 2087 | CallbackData& handler = handlersForEvent[i]; | ||
| 2088 | callWithEnvironment(handler.definingEntityIdentifier, handler.definingSandboxURL, handler.function, QScriptValue(), eventHandlerArgs); | ||
| 2089 | } | ||
| 2090 | } | ||
| 2091 | } | ||
| 2092 | |||
| 2093 | int ScriptEngine::getNumRunningEntityScripts() const { | ||
| 2094 | QReadLocker locker { &_entityScriptsLock }; | ||
| 2095 | int sum = 0; | ||
| 2096 | for (const auto& st : _entityScripts) { | ||
| 2097 | if (st.status == EntityScriptStatus::RUNNING) { | ||
| 2098 | ++sum; | ||
| 2099 | } | ||
| 2100 | } | ||
| 2101 | return sum; | ||
| 2102 | } | ||
| 2103 | |||
| 2104 | void ScriptEngine::setEntityScriptDetails(const EntityItemID& entityID, const EntityScriptDetails& details) { | ||
| 2105 | { | ||
| 2106 | QWriteLocker locker { &_entityScriptsLock }; | ||
| 2107 | _entityScripts[entityID] = details; | ||
| 2108 | } | ||
| 2109 | emit entityScriptDetailsUpdated(); | ||
| 2110 | } | ||
| 2111 | |||
| 2112 | void ScriptEngine::updateEntityScriptStatus(const EntityItemID& entityID, const EntityScriptStatus &status, const QString& errorInfo) { | ||
| 2113 | { | ||
| 2114 | QWriteLocker locker { &_entityScriptsLock }; | ||
| 2115 | EntityScriptDetails& details = _entityScripts[entityID]; | ||
| 2116 | details.status = status; | ||
| 2117 | details.errorInfo = errorInfo; | ||
| 2118 | } | ||
| 2119 | emit entityScriptDetailsUpdated(); | ||
| 2120 | } | ||
| 2121 | |||
| 2122 | QVariant ScriptEngine::cloneEntityScriptDetails(const EntityItemID& entityID) { | ||
| 2123 | static const QVariant NULL_VARIANT = QVariant::fromValue(nullptr); | ||
| 2124 | QVariantMap map; | ||
| 2125 | if (entityID.isNull()) { | ||
| 2126 | // TODO: find better way to report JS Error across thread/process boundaries | ||
| 2127 | map["isError"] = true; | ||
| 2128 | map["errorInfo"] = "Error: getEntityScriptDetails -- invalid entityID"; | ||
| 2129 | } else { | ||
| 2130 | #ifdef DEBUG_ENTITY_STATES | ||
| 2131 | qDebug() << "cloneEntityScriptDetails" << entityID << QThread::currentThread(); | ||
| 2132 | #endif | ||
| 2133 | EntityScriptDetails scriptDetails; | ||
| 2134 | if (getEntityScriptDetails(entityID, scriptDetails)) { | ||
| 2135 | #ifdef DEBUG_ENTITY_STATES | ||
| 2136 | qDebug() << "gotEntityScriptDetails" << scriptDetails.status << QThread::currentThread(); | ||
| 2137 | #endif | ||
| 2138 | map["isRunning"] = isEntityScriptRunning(entityID); | ||
| 2139 | map["status"] = EntityScriptStatus_::valueToKey(scriptDetails.status).toLower(); | ||
| 2140 | map["errorInfo"] = scriptDetails.errorInfo; | ||
| 2141 | map["entityID"] = entityID.toString(); | ||
| 2142 | #ifdef DEBUG_ENTITY_STATES | ||
| 2143 | { | ||
| 2144 | auto debug = QVariantMap(); | ||
| 2145 | debug["script"] = scriptDetails.scriptText; | ||
| 2146 | debug["scriptObject"] = scriptDetails.scriptObject.toVariant(); | ||
| 2147 | debug["lastModified"] = (qlonglong)scriptDetails.lastModified; | ||
| 2148 | debug["sandboxURL"] = scriptDetails.definingSandboxURL; | ||
| 2149 | map["debug"] = debug; | ||
| 2150 | } | ||
| 2151 | #endif | ||
| 2152 | } else { | ||
| 2153 | #ifdef DEBUG_ENTITY_STATES | ||
| 2154 | qDebug() << "!gotEntityScriptDetails" << QThread::currentThread(); | ||
| 2155 | #endif | ||
| 2156 | map["isError"] = true; | ||
| 2157 | map["errorInfo"] = "Entity script details unavailable"; | ||
| 2158 | map["entityID"] = entityID.toString(); | ||
| 2159 | } | ||
| 2160 | } | ||
| 2161 | return map; | ||
| 2162 | } | ||
| 2163 | |||
| 2164 | QFuture<QVariant> ScriptEngine::getLocalEntityScriptDetails(const EntityItemID& entityID) { | ||
| 2165 | return QtConcurrent::run(this, &ScriptEngine::cloneEntityScriptDetails, entityID); | ||
| 2166 | } | ||
| 2167 | |||
| 2168 | bool ScriptEngine::getEntityScriptDetails(const EntityItemID& entityID, EntityScriptDetails &details) const { | ||
| 2169 | QReadLocker locker { &_entityScriptsLock }; | ||
| 2170 | auto it = _entityScripts.constFind(entityID); | ||
| 2171 | if (it == _entityScripts.constEnd()) { | ||
| 2172 | return false; | ||
| 2173 | } | ||
| 2174 | details = it.value(); | ||
| 2175 | return true; | ||
| 2176 | } | ||
| 2177 | |||
| 2178 | bool ScriptEngine::hasEntityScriptDetails(const EntityItemID& entityID) const { | ||
| 2179 | QReadLocker locker { &_entityScriptsLock }; | ||
| 2180 | return _entityScripts.contains(entityID); | ||
| 2181 | } | ||
| 2182 | |||
| 2183 | void ScriptEngine::loadEntityScript(const EntityItemID& entityID, const QString& entityScript, bool forceRedownload) { | ||
| 2184 | if (QThread::currentThread() != thread()) { | ||
| 2185 | QMetaObject::invokeMethod(this, "loadEntityScript", | ||
| 2186 | Q_ARG(const EntityItemID&, entityID), | ||
| 2187 | Q_ARG(const QString&, entityScript), | ||
| 2188 | Q_ARG(bool, forceRedownload) | ||
| 2189 | ); | ||
| 2190 | return; | ||
| 2191 | } | ||
| 2192 | PROFILE_RANGE(script, __FUNCTION__); | ||
| 2193 | |||
| 2194 | QSharedPointer<ScriptEngines> scriptEngines(_scriptEngines); | ||
| 2195 | if (isStopping() || !scriptEngines || scriptEngines->isStopped()) { | ||
| 2196 | qCDebug(scriptengine) << "loadEntityScript.start " << entityID.toString() | ||
| 2197 | << " but isStopping==" << isStopping() | ||
| 2198 | << " || engines->isStopped==" << scriptEngines->isStopped(); | ||
| 2199 | return; | ||
| 2200 | } | ||
| 2201 | |||
| 2202 | if (!hasEntityScriptDetails(entityID)) { | ||
| 2203 | // make sure EntityScriptDetails has an entry for this UUID right away | ||
| 2204 | // (which allows bailing from the loading/provisioning process early if the Entity gets deleted mid-flight) | ||
| 2205 | updateEntityScriptStatus(entityID, EntityScriptStatus::PENDING, "...pending..."); | ||
| 2206 | } | ||
| 2207 | |||
| 2208 | #ifdef DEBUG_ENTITY_STATES | ||
| 2209 | { | ||
| 2210 | EntityScriptDetails details; | ||
| 2211 | bool hasEntityScript = getEntityScriptDetails(entityID, details); | ||
| 2212 | qCDebug(scriptengine) << "loadEntityScript.LOADING: " << entityID.toString() | ||
| 2213 | << "(previous: " << (hasEntityScript ? details.status : EntityScriptStatus::PENDING) << ")"; | ||
| 2214 | } | ||
| 2215 | #endif | ||
| 2216 | |||
| 2217 | EntityScriptDetails newDetails; | ||
| 2218 | newDetails.scriptText = entityScript; | ||
| 2219 | newDetails.status = EntityScriptStatus::LOADING; | ||
| 2220 | newDetails.definingSandboxURL = currentSandboxURL; | ||
| 2221 | setEntityScriptDetails(entityID, newDetails); | ||
| 2222 | |||
| 2223 | auto scriptCache = DependencyManager::get<ScriptCache>(); | ||
| 2224 | // note: see EntityTreeRenderer.cpp for shared pointer lifecycle management | ||
| 2225 | QWeakPointer<BaseScriptEngine> weakRef(sharedFromThis()); | ||
| 2226 | scriptCache->getScriptContents(entityScript, | ||
| 2227 | [this, weakRef, entityScript, entityID](const QString& url, const QString& contents, bool isURL, bool success, const QString& status) { | ||
| 2228 | QSharedPointer<BaseScriptEngine> strongRef(weakRef); | ||
| 2229 | if (!strongRef) { | ||
| 2230 | qCWarning(scriptengine) << "loadEntityScript.contentAvailable -- ScriptEngine was deleted during getScriptContents!!"; | ||
| 2231 | return; | ||
| 2232 | } | ||
| 2233 | if (isStopping()) { | ||
| 2234 | #ifdef DEBUG_ENTITY_STATES | ||
| 2235 | qCDebug(scriptengine) << "loadEntityScript.contentAvailable -- stopping"; | ||
| 2236 | #endif | ||
| 2237 | return; | ||
| 2238 | } | ||
| 2239 | executeOnScriptThread([=]{ | ||
| 2240 | #ifdef DEBUG_ENTITY_STATES | ||
| 2241 | qCDebug(scriptengine) << "loadEntityScript.contentAvailable" << status << entityID.toString(); | ||
| 2242 | #endif | ||
| 2243 | if (!isStopping() && hasEntityScriptDetails(entityID)) { | ||
| 2244 | _contentAvailableQueue[entityID] = { entityID, url, contents, isURL, success, status }; | ||
| 2245 | } else { | ||
| 2246 | #ifdef DEBUG_ENTITY_STATES | ||
| 2247 | qCDebug(scriptengine) << "loadEntityScript.contentAvailable -- aborting"; | ||
| 2248 | #endif | ||
| 2249 | } | ||
| 2250 | }); | ||
| 2251 | }, forceRedownload); | ||
| 2252 | } | ||
| 2253 | |||
| 2254 | /*@jsdoc | ||
| 2255 | * Triggered when the script starts for a user. See also, {@link Script.entityScriptPreloadFinished}. | ||
| 2256 | * <p>Note: Can only be connected to via <code>this.preload = function (...) { ... }</code> in the entity script.</p> | ||
| 2257 | * <p class="availableIn"><strong>Supported Script Types:</strong> Client Entity Scripts • Server Entity Scripts</p> | ||
| 2258 | * @function Entities.preload | ||
| 2259 | * @param {Uuid} entityID - The ID of the entity that the script is running in. | ||
| 2260 | * @returns {Signal} | ||
| 2261 | * @example <caption>Get the ID of the entity that a client entity script is running in.</caption> | ||
| 2262 | * var entityScript = (function () { | ||
| 2263 | * this.entityID = Uuid.NULL; | ||
| 2264 | * | ||
| 2265 | * this.preload = function (entityID) { | ||
| 2266 | * this.entityID = entityID; | ||
| 2267 | * print("Entity ID: " + this.entityID); | ||
| 2268 | * }; | ||
| 2269 | * }); | ||
| 2270 | * | ||
| 2271 | * var entityID = Entities.addEntity({ | ||
| 2272 | * type: "Box", | ||
| 2273 | * position: Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, { x: 0, y: 0, z: -5 })), | ||
| 2274 | * dimensions: { x: 0.5, y: 0.5, z: 0.5 }, | ||
| 2275 | * color: { red: 255, green: 0, blue: 0 }, | ||
| 2276 | * script: "(" + entityScript + ")", // Could host the script on a Web server instead. | ||
| 2277 | * lifetime: 300 // Delete after 5 minutes. | ||
| 2278 | * }); | ||
| 2279 | */ | ||
| 2280 | // The JSDoc is for the callEntityScriptMethod() call in this method. | ||
| 2281 | // since all of these operations can be asynch we will always do the actual work in the response handler | ||
| 2282 | // for the download | ||
| 2283 | void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, const QString& scriptOrURL, const QString& contents, bool isURL, bool success , const QString& status) { | ||
| 2284 | if (QThread::currentThread() != thread()) { | ||
| 2285 | #ifdef THREAD_DEBUGGING | ||
| 2286 | qCDebug(scriptengine) << "*** WARNING *** ScriptEngine::entityScriptContentAvailable() called on wrong thread [" | ||
| 2287 | << QThread::currentThread() << "], invoking on correct thread [" << thread() | ||
| 2288 | << "] " "entityID:" << entityID << "scriptOrURL:" << scriptOrURL << "contents:" | ||
| 2289 | << contents << "isURL:" << isURL << "success:" << success; | ||
| 2290 | #endif | ||
| 2291 | |||
| 2292 | QMetaObject::invokeMethod(this, "entityScriptContentAvailable", | ||
| 2293 | Q_ARG(const EntityItemID&, entityID), | ||
| 2294 | Q_ARG(const QString&, scriptOrURL), | ||
| 2295 | Q_ARG(const QString&, contents), | ||
| 2296 | Q_ARG(bool, isURL), | ||
| 2297 | Q_ARG(bool, success), | ||
| 2298 | Q_ARG(const QString&, status)); | ||
| 2299 | return; | ||
| 2300 | } | ||
| 2301 | |||
| 2302 | #ifdef THREAD_DEBUGGING | ||
| 2303 | qCDebug(scriptengine) << "ScriptEngine::entityScriptContentAvailable() thread [" << QThread::currentThread() << "] expected thread [" << thread() << "]"; | ||
| 2304 | #endif | ||
| 2305 | |||
| 2306 | auto scriptCache = DependencyManager::get<ScriptCache>(); | ||
| 2307 | bool isFileUrl = isURL && scriptOrURL.startsWith("file://"); | ||
| 2308 | auto fileName = isURL ? scriptOrURL : "about:EmbeddedEntityScript"; | ||
| 2309 | |||
| 2310 | QString entityScript; | ||
| 2311 | { | ||
| 2312 | QWriteLocker locker { &_entityScriptsLock }; | ||
| 2313 | entityScript = _entityScripts[entityID].scriptText; | ||
| 2314 | } | ||
| 2315 | |||
| 2316 | EntityScriptDetails newDetails; | ||
| 2317 | newDetails.scriptText = scriptOrURL; | ||
| 2318 | |||
| 2319 | // If an error happens below, we want to update newDetails with the new status info | ||
| 2320 | // and also abort any pending Entity loads that are waiting on the exact same script URL. | ||
| 2321 | auto setError = [&](const QString &errorInfo, const EntityScriptStatus& status) { | ||
| 2322 | newDetails.errorInfo = errorInfo; | ||
| 2323 | newDetails.status = status; | ||
| 2324 | setEntityScriptDetails(entityID, newDetails); | ||
| 2325 | }; | ||
| 2326 | |||
| 2327 | // NETWORK / FILESYSTEM ERRORS | ||
| 2328 | if (!success) { | ||
| 2329 | setError("Failed to load script (" + status + ")", EntityScriptStatus::ERROR_LOADING_SCRIPT); | ||
| 2330 | return; | ||
| 2331 | } | ||
| 2332 | |||
| 2333 | // SYNTAX ERRORS | ||
| 2334 | auto syntaxError = lintScript(contents, fileName); | ||
| 2335 | if (syntaxError.isError()) { | ||
| 2336 | auto message = syntaxError.property("formatted").toString(); | ||
| 2337 | if (message.isEmpty()) { | ||
| 2338 | message = syntaxError.toString(); | ||
| 2339 | } | ||
| 2340 | setError(QString("Bad syntax (%1)").arg(message), EntityScriptStatus::ERROR_RUNNING_SCRIPT); | ||
| 2341 | syntaxError.setProperty("detail", entityID.toString()); | ||
| 2342 | emit unhandledException(syntaxError); | ||
| 2343 | return; | ||
| 2344 | } | ||
| 2345 | QScriptProgram program { contents, fileName }; | ||
| 2346 | if (program.isNull()) { | ||
| 2347 | setError("Bad program (isNull)", EntityScriptStatus::ERROR_RUNNING_SCRIPT); | ||
| 2348 | emit unhandledException(makeError("program.isNull")); | ||
| 2349 | return; // done processing script | ||
| 2350 | } | ||
| 2351 | |||
| 2352 | if (isURL) { | ||
| 2353 | setParentURL(scriptOrURL); | ||
| 2354 | } | ||
| 2355 | |||
| 2356 | // SANITY/PERFORMANCE CHECK USING SANDBOX | ||
| 2357 | const int SANDBOX_TIMEOUT = 0.25 * MSECS_PER_SECOND; | ||
| 2358 | BaseScriptEngine sandbox; | ||
| 2359 | sandbox.setProcessEventsInterval(SANDBOX_TIMEOUT); | ||
| 2360 | QScriptValue testConstructor, exception; | ||
| 2361 | if (atoi(getenv("UNSAFE_ENTITY_SCRIPTS") ? getenv("UNSAFE_ENTITY_SCRIPTS") : "0")) | ||
| 2362 | { | ||
| 2363 | QTimer timeout; | ||
| 2364 | timeout.setSingleShot(true); | ||
| 2365 | timeout.start(SANDBOX_TIMEOUT); | ||
| 2366 | connect(&timeout, &QTimer::timeout, [=, &sandbox]{ | ||
| 2367 | qCDebug(scriptengine) << "ScriptEngine::entityScriptContentAvailable timeout"; | ||
| 2368 | |||
| 2369 | // Guard against infinite loops and non-performant code | ||
| 2370 | sandbox.raiseException( | ||
| 2371 | sandbox.makeError(QString("Timed out (entity constructors are limited to %1ms)").arg(SANDBOX_TIMEOUT)) | ||
| 2372 | ); | ||
| 2373 | }); | ||
| 2374 | |||
| 2375 | testConstructor = sandbox.evaluate(program); | ||
| 2376 | |||
| 2377 | if (sandbox.hasUncaughtException()) { | ||
| 2378 | exception = sandbox.cloneUncaughtException(QString("(preflight %1)").arg(entityID.toString())); | ||
| 2379 | sandbox.clearExceptions(); | ||
| 2380 | } else if (testConstructor.isError()) { | ||
| 2381 | exception = testConstructor; | ||
| 2382 | } | ||
| 2383 | } else { | ||
| 2384 | // ENTITY SCRIPT WHITELIST STARTS HERE | ||
| 2385 | auto nodeList = DependencyManager::get<NodeList>(); | ||
| 2386 | bool passList = false; // assume unsafe | ||
| 2387 | QString whitelistPrefix = "[WHITELIST ENTITY SCRIPTS]"; | ||
| 2388 | QList<QString> safeURLPrefixes = { "file:///", "atp:", "cache:" }; | ||
| 2389 | safeURLPrefixes += qEnvironmentVariable("EXTRA_WHITELIST").trimmed().split(QRegExp("\\s*,\\s*"), Qt::SkipEmptyParts); | ||
| 2390 | |||
| 2391 | // Entity Script Whitelist toggle check. | ||
| 2392 | Setting::Handle<bool> whitelistEnabled {"private/whitelistEnabled", false }; | ||
| 2393 | |||
| 2394 | if (!whitelistEnabled.get()) { | ||
| 2395 | passList = true; | ||
| 2396 | } | ||
| 2397 | |||
| 2398 | // Pull SAFEURLS from the Interface.JSON settings. | ||
| 2399 | QVariant raw = Setting::Handle<QVariant>("private/settingsSafeURLS").get(); | ||
| 2400 | QStringList settingsSafeURLS = raw.toString().trimmed().split(QRegExp("\\s*[,\r\n]+\\s*"), Qt::SkipEmptyParts); | ||
| 2401 | safeURLPrefixes += settingsSafeURLS; | ||
| 2402 | // END Pull SAFEURLS from the Interface.JSON settings. | ||
| 2403 | |||
| 2404 | // Get current domain whitelist bypass, in case an entire domain is whitelisted. | ||
| 2405 | QString currentDomain = DependencyManager::get<AddressManager>()->getDomainURL().host(); | ||
| 2406 | |||
| 2407 | QString domainSafeIP = nodeList->getDomainHandler().getHostname(); | ||
| 2408 | QString domainSafeURL = URL_SCHEME_OVERTE + "://" + currentDomain; | ||
| 2409 | for (const auto& str : safeURLPrefixes) { | ||
| 2410 | if (domainSafeURL.startsWith(str) || domainSafeIP.startsWith(str)) { | ||
| 2411 | qCDebug(scriptengine) << whitelistPrefix << "Whitelist Bypassed, entire domain is whitelisted. Current Domain Host: " | ||
| 2412 | << nodeList->getDomainHandler().getHostname() | ||
| 2413 | << "Current Domain: " << currentDomain; | ||
| 2414 | passList = true; | ||
| 2415 | } | ||
| 2416 | } | ||
| 2417 | // END bypass whitelist based on current domain. | ||
| 2418 | |||
| 2419 | // Start processing scripts through the whitelist. | ||
| 2420 | if (ScriptEngine::getContext() == "entity_server") { // If running on the server, do not engage whitelist. | ||
| 2421 | passList = true; | ||
| 2422 | } else if (!passList) { // If waved through, do not engage whitelist. | ||
| 2423 | for (const auto& str : safeURLPrefixes) { | ||
| 2424 | qCDebug(scriptengine) << whitelistPrefix << "Script URL: " << scriptOrURL << "TESTING AGAINST" << str << "RESULTS IN" | ||
| 2425 | << scriptOrURL.startsWith(str); | ||
| 2426 | if (!str.isEmpty() && scriptOrURL.startsWith(str)) { | ||
| 2427 | passList = true; | ||
| 2428 | qCDebug(scriptengine) << whitelistPrefix << "Script approved."; | ||
| 2429 | break; // Bail early since we found a match. | ||
| 2430 | } | ||
| 2431 | } | ||
| 2432 | } | ||
| 2433 | // END processing of scripts through the whitelist. | ||
| 2434 | |||
| 2435 | if (!passList) { // If the entity failed to pass for any reason, it's blocked and an error is thrown. | ||
| 2436 | qCDebug(scriptengine) << whitelistPrefix << "(disabled entity script)" << entityID.toString() << scriptOrURL; | ||
| 2437 | exception = makeError("UNSAFE_ENTITY_SCRIPTS == 0"); | ||
| 2438 | } else { | ||
| 2439 | QTimer timeout; | ||
| 2440 | timeout.setSingleShot(true); | ||
| 2441 | timeout.start(SANDBOX_TIMEOUT); | ||
| 2442 | connect(&timeout, &QTimer::timeout, [=, &sandbox] { | ||
| 2443 | qCDebug(scriptengine) << "ScriptEngine::entityScriptContentAvailable timeout"; | ||
| 2444 | |||
| 2445 | // Guard against infinite loops and non-performant code | ||
| 2446 | sandbox.raiseException( | ||
| 2447 | sandbox.makeError(QString("Timed out (entity constructors are limited to %1ms)").arg(SANDBOX_TIMEOUT))); | ||
| 2448 | }); | ||
| 2449 | |||
| 2450 | testConstructor = sandbox.evaluate(program); | ||
| 2451 | |||
| 2452 | if (sandbox.hasUncaughtException()) { | ||
| 2453 | exception = sandbox.cloneUncaughtException(QString("(preflight %1)").arg(entityID.toString())); | ||
| 2454 | sandbox.clearExceptions(); | ||
| 2455 | } else if (testConstructor.isError()) { | ||
| 2456 | exception = testConstructor; | ||
| 2457 | } | ||
| 2458 | } | ||
| 2459 | // ENTITY SCRIPT WHITELIST ENDS HERE, uncomment below for original full disabling. | ||
| 2460 | |||
| 2461 | // qDebug() << "(disabled entity script)" << entityID.toString() << scriptOrURL; | ||
| 2462 | // exception = makeError("UNSAFE_ENTITY_SCRIPTS == 0"); | ||
| 2463 | } | ||
| 2464 | |||
| 2465 | if (exception.isError()) { | ||
| 2466 | // create a local copy using makeError to decouple from the sandbox engine | ||
| 2467 | exception = makeError(exception); | ||
| 2468 | setError(formatException(exception, _enableExtendedJSExceptions.get()), EntityScriptStatus::ERROR_RUNNING_SCRIPT); | ||
| 2469 | emit unhandledException(exception); | ||
| 2470 | return; | ||
| 2471 | } | ||
| 2472 | |||
| 2473 | // CONSTRUCTOR VIABILITY | ||
| 2474 | if (!testConstructor.isFunction()) { | ||
| 2475 | QString testConstructorType = QString(testConstructor.toVariant().typeName()); | ||
| 2476 | if (testConstructorType == "") { | ||
| 2477 | testConstructorType = "empty"; | ||
| 2478 | } | ||
| 2479 | QString testConstructorValue = testConstructor.toString(); | ||
| 2480 | if (testConstructorValue.size() > MAX_DEBUG_VALUE_LENGTH) { | ||
| 2481 | testConstructorValue = testConstructorValue.mid(0, MAX_DEBUG_VALUE_LENGTH) + "..."; | ||
| 2482 | } | ||
| 2483 | auto message = QString("failed to load entity script -- expected a function, got %1, %2") | ||
| 2484 | .arg(testConstructorType).arg(testConstructorValue); | ||
| 2485 | |||
| 2486 | auto err = makeError(message); | ||
| 2487 | err.setProperty("fileName", scriptOrURL); | ||
| 2488 | err.setProperty("detail", "(constructor " + entityID.toString() + ")"); | ||
| 2489 | |||
| 2490 | setError("Could not find constructor (" + testConstructorType + ")", EntityScriptStatus::ERROR_RUNNING_SCRIPT); | ||
| 2491 | emit unhandledException(err); | ||
| 2492 | return; // done processing script | ||
| 2493 | } | ||
| 2494 | |||
| 2495 | // (this feeds into refreshFileScript) | ||
| 2496 | int64_t lastModified = 0; | ||
| 2497 | if (isFileUrl) { | ||
| 2498 | QString file = QUrl(scriptOrURL).toLocalFile(); | ||
| 2499 | lastModified = (quint64)QFileInfo(file).lastModified().toMSecsSinceEpoch(); | ||
| 2500 | } | ||
| 2501 | |||
| 2502 | // THE ACTUAL EVALUATION AND CONSTRUCTION | ||
| 2503 | QScriptValue entityScriptConstructor, entityScriptObject; | ||
| 2504 | QUrl sandboxURL = currentSandboxURL.isEmpty() ? scriptOrURL : currentSandboxURL; | ||
| 2505 | auto initialization = [&]{ | ||
| 2506 | entityScriptConstructor = evaluate(contents, fileName); | ||
| 2507 | entityScriptObject = entityScriptConstructor.construct(); | ||
| 2508 | |||
| 2509 | if (hasUncaughtException()) { | ||
| 2510 | entityScriptObject = cloneUncaughtException("(construct " + entityID.toString() + ")"); | ||
| 2511 | clearExceptions(); | ||
| 2512 | } | ||
| 2513 | }; | ||
| 2514 | |||
| 2515 | doWithEnvironment(entityID, sandboxURL, initialization); | ||
| 2516 | |||
| 2517 | if (entityScriptObject.isError()) { | ||
| 2518 | auto exception = entityScriptObject; | ||
| 2519 | setError(formatException(exception, _enableExtendedJSExceptions.get()), EntityScriptStatus::ERROR_RUNNING_SCRIPT); | ||
| 2520 | emit unhandledException(exception); | ||
| 2521 | return; | ||
| 2522 | } | ||
| 2523 | |||
| 2524 | // ... AND WE HAVE LIFTOFF | ||
| 2525 | newDetails.status = EntityScriptStatus::RUNNING; | ||
| 2526 | newDetails.scriptObject = entityScriptObject; | ||
| 2527 | newDetails.lastModified = lastModified; | ||
| 2528 | newDetails.definingSandboxURL = sandboxURL; | ||
| 2529 | setEntityScriptDetails(entityID, newDetails); | ||
| 2530 | |||
| 2531 | if (isURL) { | ||
| 2532 | setParentURL(""); | ||
| 2533 | } | ||
| 2534 | |||
| 2535 | // if we got this far, then call the preload method | ||
| 2536 | callEntityScriptMethod(entityID, "preload"); | ||
| 2537 | |||
| 2538 | emit entityScriptPreloadFinished(entityID); | ||
| 2539 | } | ||
| 2540 | |||
| 2541 | /*@jsdoc | ||
| 2542 | * Triggered when the script terminates for a user. | ||
| 2543 | * <p>Note: Can only be connected to via <code>this.unoad = function () { ... }</code> in the entity script.</p> | ||
| 2544 | * <p class="availableIn"><strong>Supported Script Types:</strong> Client Entity Scripts • Server Entity Scripts</p> | ||
| 2545 | * @function Entities.unload | ||
| 2546 | * @param {Uuid} entityID - The ID of the entity that the script is running in. | ||
| 2547 | * @returns {Signal} | ||
| 2548 | */ | ||
| 2549 | // The JSDoc is for the callEntityScriptMethod() call in this method. | ||
| 2550 | void ScriptEngine::unloadEntityScript(const EntityItemID& entityID, bool shouldRemoveFromMap) { | ||
| 2551 | if (QThread::currentThread() != thread()) { | ||
| 2552 | #ifdef THREAD_DEBUGGING | ||
| 2553 | qCDebug(scriptengine) << "*** WARNING *** ScriptEngine::unloadEntityScript() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] " | ||
| 2554 | "entityID:" << entityID; | ||
| 2555 | #endif | ||
| 2556 | |||
| 2557 | QMetaObject::invokeMethod(this, "unloadEntityScript", | ||
| 2558 | Q_ARG(const EntityItemID&, entityID), | ||
| 2559 | Q_ARG(bool, shouldRemoveFromMap)); | ||
| 2560 | return; | ||
| 2561 | } | ||
| 2562 | #ifdef THREAD_DEBUGGING | ||
| 2563 | qCDebug(scriptengine) << "ScriptEngine::unloadEntityScript() called on correct thread [" << thread() << "] " | ||
| 2564 | "entityID:" << entityID; | ||
| 2565 | #endif | ||
| 2566 | |||
| 2567 | EntityScriptDetails oldDetails; | ||
| 2568 | if (getEntityScriptDetails(entityID, oldDetails)) { | ||
| 2569 | auto scriptText = oldDetails.scriptText; | ||
| 2570 | |||
| 2571 | if (isEntityScriptRunning(entityID)) { | ||
| 2572 | callEntityScriptMethod(entityID, "unload"); | ||
| 2573 | } | ||
| 2574 | #ifdef DEBUG_ENTITY_STATES | ||
| 2575 | else { | ||
| 2576 | qCDebug(scriptengine) << "unload called while !running" << entityID << oldDetails.status; | ||
| 2577 | } | ||
| 2578 | #endif | ||
| 2579 | if (shouldRemoveFromMap) { | ||
| 2580 | // this was a deleted entity, we've been asked to remove it from the map | ||
| 2581 | { | ||
| 2582 | QWriteLocker locker { &_entityScriptsLock }; | ||
| 2583 | _entityScripts.remove(entityID); | ||
| 2584 | } | ||
| 2585 | emit entityScriptDetailsUpdated(); | ||
| 2586 | } else if (oldDetails.status != EntityScriptStatus::UNLOADED) { | ||
| 2587 | EntityScriptDetails newDetails; | ||
| 2588 | newDetails.status = EntityScriptStatus::UNLOADED; | ||
| 2589 | newDetails.lastModified = QDateTime::currentMSecsSinceEpoch(); | ||
| 2590 | // keep scriptText populated for the current need to "debouce" duplicate calls to unloadEntityScript | ||
| 2591 | newDetails.scriptText = scriptText; | ||
| 2592 | setEntityScriptDetails(entityID, newDetails); | ||
| 2593 | } | ||
| 2594 | |||
| 2595 | stopAllTimersForEntityScript(entityID); | ||
| 2596 | } | ||
| 2597 | } | ||
| 2598 | |||
| 2599 | QList<EntityItemID> ScriptEngine::getListOfEntityScriptIDs() { | ||
| 2600 | QReadLocker locker{ &_entityScriptsLock }; | ||
| 2601 | return _entityScripts.keys(); | ||
| 2602 | } | ||
| 2603 | |||
| 2604 | void ScriptEngine::unloadAllEntityScripts(bool blockingCall) { | ||
| 2605 | if (QThread::currentThread() != thread()) { | ||
| 2606 | #ifdef THREAD_DEBUGGING | ||
| 2607 | qCDebug(scriptengine) << "*** WARNING *** ScriptEngine::unloadAllEntityScripts() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "]"; | ||
| 2608 | #endif | ||
| 2609 | |||
| 2610 | QMetaObject::invokeMethod(this, "unloadAllEntityScripts", | ||
| 2611 | blockingCall ? Qt::BlockingQueuedConnection : Qt::QueuedConnection); | ||
| 2612 | return; | ||
| 2613 | } | ||
| 2614 | #ifdef THREAD_DEBUGGING | ||
| 2615 | qCDebug(scriptengine) << "ScriptEngine::unloadAllEntityScripts() called on correct thread [" << thread() << "]"; | ||
| 2616 | #endif | ||
| 2617 | |||
| 2618 | QList<EntityItemID> keys; | ||
| 2619 | { | ||
| 2620 | QReadLocker locker{ &_entityScriptsLock }; | ||
| 2621 | keys = _entityScripts.keys(); | ||
| 2622 | } | ||
| 2623 | foreach(const EntityItemID& entityID, keys) { | ||
| 2624 | unloadEntityScript(entityID); | ||
| 2625 | } | ||
| 2626 | { | ||
| 2627 | QWriteLocker locker{ &_entityScriptsLock }; | ||
| 2628 | _entityScripts.clear(); | ||
| 2629 | } | ||
| 2630 | emit entityScriptDetailsUpdated(); | ||
| 2631 | |||
| 2632 | #ifdef DEBUG_ENGINE_STATE | ||
| 2633 | _debugDump( | ||
| 2634 | "---- CURRENT STATE OF ENGINE: --------------------------", | ||
| 2635 | globalObject(), | ||
| 2636 | "--------------------------------------------------------" | ||
| 2637 | ); | ||
| 2638 | #endif // DEBUG_ENGINE_STATE | ||
| 2639 | } | ||
| 2640 | |||
| 2641 | void ScriptEngine::refreshFileScript(const EntityItemID& entityID) { | ||
| 2642 | if (!HIFI_AUTOREFRESH_FILE_SCRIPTS || !hasEntityScriptDetails(entityID)) { | ||
| 2643 | return; | ||
| 2644 | } | ||
| 2645 | |||
| 2646 | static bool recurseGuard = false; | ||
| 2647 | if (recurseGuard) { | ||
| 2648 | return; | ||
| 2649 | } | ||
| 2650 | recurseGuard = true; | ||
| 2651 | |||
| 2652 | EntityScriptDetails details; | ||
| 2653 | { | ||
| 2654 | QWriteLocker locker { &_entityScriptsLock }; | ||
| 2655 | details = _entityScripts[entityID]; | ||
| 2656 | } | ||
| 2657 | // Check to see if a file based script needs to be reloaded (easier debugging) | ||
| 2658 | if (details.lastModified > 0) { | ||
| 2659 | QString filePath = QUrl(details.scriptText).toLocalFile(); | ||
| 2660 | auto lastModified = QFileInfo(filePath).lastModified().toMSecsSinceEpoch(); | ||
| 2661 | if (lastModified > details.lastModified) { | ||
| 2662 | scriptInfoMessage("Reloading modified script " + details.scriptText); | ||
| 2663 | loadEntityScript(entityID, details.scriptText, true); | ||
| 2664 | } | ||
| 2665 | } | ||
| 2666 | recurseGuard = false; | ||
| 2667 | } | ||
| 2668 | |||
| 2669 | // Execute operation in the appropriate context for (the possibly empty) entityID. | ||
| 2670 | // Even if entityID is supplied as currentEntityIdentifier, this still documents the source | ||
| 2671 | // of the code being executed (e.g., if we ever sandbox different entity scripts, or provide different | ||
| 2672 | // global values for different entity scripts). | ||
| 2673 | void ScriptEngine::doWithEnvironment(const EntityItemID& entityID, const QUrl& sandboxURL, std::function<void()> operation) { | ||
| 2674 | EntityItemID oldIdentifier = currentEntityIdentifier; | ||
| 2675 | QUrl oldSandboxURL = currentSandboxURL; | ||
| 2676 | currentEntityIdentifier = entityID; | ||
| 2677 | currentSandboxURL = sandboxURL; | ||
| 2678 | |||
| 2679 | #if DEBUG_CURRENT_ENTITY | ||
| 2680 | QScriptValue oldData = this->globalObject().property("debugEntityID"); | ||
| 2681 | this->globalObject().setProperty("debugEntityID", entityID.toScriptValue(this)); // Make the entityID available to javascript as a global. | ||
| 2682 | operation(); | ||
| 2683 | this->globalObject().setProperty("debugEntityID", oldData); | ||
| 2684 | #else | ||
| 2685 | operation(); | ||
| 2686 | #endif | ||
| 2687 | maybeEmitUncaughtException(!entityID.isNull() ? entityID.toString() : __FUNCTION__); | ||
| 2688 | currentEntityIdentifier = oldIdentifier; | ||
| 2689 | currentSandboxURL = oldSandboxURL; | ||
| 2690 | } | ||
| 2691 | |||
| 2692 | void ScriptEngine::callWithEnvironment(const EntityItemID& entityID, const QUrl& sandboxURL, QScriptValue function, QScriptValue thisObject, QScriptValueList args) { | ||
| 2693 | auto operation = [&]() { | ||
| 2694 | function.call(thisObject, args); | ||
| 2695 | }; | ||
| 2696 | doWithEnvironment(entityID, sandboxURL, operation); | ||
| 2697 | } | ||
| 2698 | |||
| 2699 | void ScriptEngine::callEntityScriptMethod(const EntityItemID& entityID, const QString& methodName, const QStringList& params, const QUuid& remoteCallerID) { | ||
| 2700 | if (QThread::currentThread() != thread()) { | ||
| 2701 | #ifdef THREAD_DEBUGGING | ||
| 2702 | qCDebug(scriptengine) << "*** WARNING *** ScriptEngine::callEntityScriptMethod() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] " | ||
| 2703 | "entityID:" << entityID << "methodName:" << methodName; | ||
| 2704 | #endif | ||
| 2705 | |||
| 2706 | QMetaObject::invokeMethod(this, "callEntityScriptMethod", | ||
| 2707 | Q_ARG(const EntityItemID&, entityID), | ||
| 2708 | Q_ARG(const QString&, methodName), | ||
| 2709 | Q_ARG(const QStringList&, params), | ||
| 2710 | Q_ARG(const QUuid&, remoteCallerID)); | ||
| 2711 | return; | ||
| 2712 | } | ||
| 2713 | #ifdef THREAD_DEBUGGING | ||
| 2714 | qCDebug(scriptengine) << "ScriptEngine::callEntityScriptMethod() called on correct thread [" << thread() << "] " | ||
| 2715 | "entityID:" << entityID << "methodName:" << methodName; | ||
| 2716 | #endif | ||
| 2717 | |||
| 2718 | if (HIFI_AUTOREFRESH_FILE_SCRIPTS && methodName != "unload") { | ||
| 2719 | refreshFileScript(entityID); | ||
| 2720 | } | ||
| 2721 | if (isEntityScriptRunning(entityID)) { | ||
| 2722 | EntityScriptDetails details; | ||
| 2723 | { | ||
| 2724 | QWriteLocker locker { &_entityScriptsLock }; | ||
| 2725 | details = _entityScripts[entityID]; | ||
| 2726 | } | ||
| 2727 | QScriptValue entityScript = details.scriptObject; // previously loaded | ||
| 2728 | |||
| 2729 | // If this is a remote call, we need to check to see if the function is remotely callable | ||
| 2730 | // we do this by checking for the existance of the 'remotelyCallable' property on the | ||
| 2731 | // entityScript. And we confirm that the method name is included. If this fails, the | ||
| 2732 | // function will not be called. | ||
| 2733 | bool callAllowed = false; | ||
| 2734 | if (remoteCallerID == QUuid()) { | ||
| 2735 | callAllowed = true; | ||
| 2736 | } else { | ||
| 2737 | if (entityScript.property("remotelyCallable").isArray()) { | ||
| 2738 | auto callables = entityScript.property("remotelyCallable"); | ||
| 2739 | auto callableCount = callables.property("length").toInteger(); | ||
| 2740 | for (int i = 0; i < callableCount; i++) { | ||
| 2741 | auto callable = callables.property(i).toString(); | ||
| 2742 | if (callable == methodName) { | ||
| 2743 | callAllowed = true; | ||
| 2744 | break; | ||
| 2745 | } | ||
| 2746 | } | ||
| 2747 | } | ||
| 2748 | if (!callAllowed) { | ||
| 2749 | qDebug() << "Method [" << methodName << "] not remotely callable."; | ||
| 2750 | } | ||
| 2751 | } | ||
| 2752 | |||
| 2753 | if (callAllowed && entityScript.property(methodName).isFunction()) { | ||
| 2754 | QScriptValueList args; | ||
| 2755 | args << entityID.toScriptValue(this); | ||
| 2756 | args << qScriptValueFromSequence(this, params); | ||
| 2757 | |||
| 2758 | QScriptValue oldData = this->globalObject().property("Script").property("remoteCallerID"); | ||
| 2759 | this->globalObject().property("Script").setProperty("remoteCallerID", remoteCallerID.toString()); // Make the remoteCallerID available to javascript as a global. | ||
| 2760 | callWithEnvironment(entityID, details.definingSandboxURL, entityScript.property(methodName), entityScript, args); | ||
| 2761 | this->globalObject().property("Script").setProperty("remoteCallerID", oldData); | ||
| 2762 | } | ||
| 2763 | } | ||
| 2764 | } | ||
| 2765 | |||
| 2766 | void ScriptEngine::callEntityScriptMethod(const EntityItemID& entityID, const QString& methodName, const PointerEvent& event) { | ||
| 2767 | if (QThread::currentThread() != thread()) { | ||
| 2768 | #ifdef THREAD_DEBUGGING | ||
| 2769 | qCDebug(scriptengine) << "*** WARNING *** ScriptEngine::callEntityScriptMethod() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] " | ||
| 2770 | "entityID:" << entityID << "methodName:" << methodName << "event: mouseEvent"; | ||
| 2771 | #endif | ||
| 2772 | |||
| 2773 | QMetaObject::invokeMethod(this, "callEntityScriptMethod", | ||
| 2774 | Q_ARG(const EntityItemID&, entityID), | ||
| 2775 | Q_ARG(const QString&, methodName), | ||
| 2776 | Q_ARG(const PointerEvent&, event)); | ||
| 2777 | return; | ||
| 2778 | } | ||
| 2779 | #ifdef THREAD_DEBUGGING | ||
| 2780 | qCDebug(scriptengine) << "ScriptEngine::callEntityScriptMethod() called on correct thread [" << thread() << "] " | ||
| 2781 | "entityID:" << entityID << "methodName:" << methodName << "event: pointerEvent"; | ||
| 2782 | #endif | ||
| 2783 | |||
| 2784 | if (HIFI_AUTOREFRESH_FILE_SCRIPTS) { | ||
| 2785 | refreshFileScript(entityID); | ||
| 2786 | } | ||
| 2787 | if (isEntityScriptRunning(entityID)) { | ||
| 2788 | EntityScriptDetails details; | ||
| 2789 | { | ||
| 2790 | QWriteLocker locker { &_entityScriptsLock }; | ||
| 2791 | details = _entityScripts[entityID]; | ||
| 2792 | } | ||
| 2793 | QScriptValue entityScript = details.scriptObject; // previously loaded | ||
| 2794 | if (entityScript.property(methodName).isFunction()) { | ||
| 2795 | QScriptValueList args; | ||
| 2796 | args << entityID.toScriptValue(this); | ||
| 2797 | args << event.toScriptValue(this); | ||
| 2798 | callWithEnvironment(entityID, details.definingSandboxURL, entityScript.property(methodName), entityScript, args); | ||
| 2799 | } | ||
| 2800 | } | ||
| 2801 | } | ||
| 2802 | |||
| 2803 | void ScriptEngine::callEntityScriptMethod(const EntityItemID& entityID, const QString& methodName, const EntityItemID& otherID, const Collision& collision) { | ||
| 2804 | if (QThread::currentThread() != thread()) { | ||
| 2805 | #ifdef THREAD_DEBUGGING | ||
| 2806 | qCDebug(scriptengine) << "*** WARNING *** ScriptEngine::callEntityScriptMethod() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] " | ||
| 2807 | "entityID:" << entityID << "methodName:" << methodName << "otherID:" << otherID << "collision: collision"; | ||
| 2808 | #endif | ||
| 2809 | |||
| 2810 | QMetaObject::invokeMethod(this, "callEntityScriptMethod", | ||
| 2811 | Q_ARG(const EntityItemID&, entityID), | ||
| 2812 | Q_ARG(const QString&, methodName), | ||
| 2813 | Q_ARG(const EntityItemID&, otherID), | ||
| 2814 | Q_ARG(const Collision&, collision)); | ||
| 2815 | return; | ||
| 2816 | } | ||
| 2817 | #ifdef THREAD_DEBUGGING | ||
| 2818 | qCDebug(scriptengine) << "ScriptEngine::callEntityScriptMethod() called on correct thread [" << thread() << "] " | ||
| 2819 | "entityID:" << entityID << "methodName:" << methodName << "otherID:" << otherID << "collision: collision"; | ||
| 2820 | #endif | ||
| 2821 | |||
| 2822 | if (HIFI_AUTOREFRESH_FILE_SCRIPTS) { | ||
| 2823 | refreshFileScript(entityID); | ||
| 2824 | } | ||
| 2825 | if (isEntityScriptRunning(entityID)) { | ||
| 2826 | EntityScriptDetails details; | ||
| 2827 | { | ||
| 2828 | QWriteLocker locker { &_entityScriptsLock }; | ||
| 2829 | details = _entityScripts[entityID]; | ||
| 2830 | } | ||
| 2831 | QScriptValue entityScript = details.scriptObject; // previously loaded | ||
| 2832 | if (entityScript.property(methodName).isFunction()) { | ||
| 2833 | QScriptValueList args; | ||
| 2834 | args << entityID.toScriptValue(this); | ||
| 2835 | args << otherID.toScriptValue(this); | ||
| 2836 | args << collisionToScriptValue(this, collision); | ||
| 2837 | callWithEnvironment(entityID, details.definingSandboxURL, entityScript.property(methodName), entityScript, args); | ||
| 2838 | } | ||
| 2839 | } | ||
| 2840 | } | ||
| 2841 | |||
| 2842 | QString ScriptEngine::getExternalPath(ExternalResource::Bucket bucket, const QString& path) { | ||
| 2843 | return ExternalResource::getInstance()->getUrl(bucket, path); | ||
| 2844 | } | ||