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/ScriptManager.cpp (1 of 260 files). Show all

Unified · Split

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