History

isolate calls to the QtScript libraries to an interface we control

Heather Anderson committed 5 years ago

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

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

Unified · Split

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