History

isolate calls to the QtScript libraries to an interface we control

Heather Anderson committed 5 years ago

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

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

Unified · Split

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