History

isolate calls to the QtScript libraries to an interface we control

Heather Anderson committed 5 years ago

Commit 1e018db1 parent 713b29e260 files · +6904 −5064Browse files

Showing the changes to libraries/script-engine/src/ScriptEngine.cpp (1 of 260 files). Show all

Unified · Split

libraries/script-engine/src/ScriptEngine.cpp+0−2844
@@ -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 &bull; 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 &bull; 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−}