mirror of
https://github.com/JulianGro/overte.git
synced 2025-04-25 18:35:04 +02:00
Merge pull request #9656 from humbletim/21114-part1
CR-1 21114 -- ScriptCache/ScriptEngine cleanup, loader states, accessible load errors
This commit is contained in:
commit
4393bc3c2e
14 changed files with 319 additions and 268 deletions
|
@ -684,22 +684,8 @@ bool EntityScriptingInterface::getServerScriptStatus(QUuid entityID, QScriptValu
|
|||
auto client = DependencyManager::get<EntityScriptClient>();
|
||||
auto request = client->createScriptStatusRequest(entityID);
|
||||
connect(request, &GetScriptStatusRequest::finished, callback.engine(), [callback](GetScriptStatusRequest* request) mutable {
|
||||
QString statusString;
|
||||
switch (request->getStatus()) {
|
||||
case RUNNING:
|
||||
statusString = "running";
|
||||
break;
|
||||
case ERROR_LOADING_SCRIPT:
|
||||
statusString = "error_loading_script";
|
||||
break;
|
||||
case ERROR_RUNNING_SCRIPT:
|
||||
statusString = "error_running_script";
|
||||
break;
|
||||
default:
|
||||
statusString = "";
|
||||
break;
|
||||
}
|
||||
QScriptValueList args { request->getResponseReceived(), request->getIsRunning(), statusString, request->getErrorInfo() };
|
||||
QString statusString = EntityScriptStatus_::valueToKey(request->getStatus());;
|
||||
QScriptValueList args { request->getResponseReceived(), request->getIsRunning(), statusString.toLower(), request->getErrorInfo() };
|
||||
callback.call(QScriptValue(), args);
|
||||
request->deleteLater();
|
||||
});
|
||||
|
|
|
@ -88,7 +88,7 @@ MessageID EntityScriptClient::getEntityServerScriptStatus(QUuid entityID, GetScr
|
|||
}
|
||||
}
|
||||
|
||||
callback(false, false, ERROR_LOADING_SCRIPT, "");
|
||||
callback(false, false, EntityScriptStatus::ERROR_LOADING_SCRIPT, "");
|
||||
return INVALID_MESSAGE_ID;
|
||||
}
|
||||
|
||||
|
@ -97,7 +97,7 @@ void EntityScriptClient::handleGetScriptStatusReply(QSharedPointer<ReceivedMessa
|
|||
|
||||
MessageID messageID;
|
||||
bool isKnown { false };
|
||||
EntityScriptStatus status = ERROR_LOADING_SCRIPT;
|
||||
EntityScriptStatus status = EntityScriptStatus::ERROR_LOADING_SCRIPT;
|
||||
QString errorInfo { "" };
|
||||
|
||||
message->readPrimitive(&messageID);
|
||||
|
@ -157,7 +157,7 @@ void EntityScriptClient::forceFailureOfPendingRequests(SharedNodePointer node) {
|
|||
auto messageMapIt = _pendingEntityScriptStatusRequests.find(node);
|
||||
if (messageMapIt != _pendingEntityScriptStatusRequests.end()) {
|
||||
for (const auto& value : messageMapIt->second) {
|
||||
value.second(false, false, ERROR_LOADING_SCRIPT, "");
|
||||
value.second(false, false, EntityScriptStatus::ERROR_LOADING_SCRIPT, "");
|
||||
}
|
||||
messageMapIt->second.clear();
|
||||
}
|
||||
|
|
|
@ -11,11 +11,23 @@
|
|||
|
||||
#ifndef hifi_EntityScriptUtils_h
|
||||
#define hifi_EntityScriptUtils_h
|
||||
#include <QMetaEnum>
|
||||
|
||||
enum EntityScriptStatus {
|
||||
ERROR_LOADING_SCRIPT,
|
||||
ERROR_RUNNING_SCRIPT,
|
||||
RUNNING
|
||||
class EntityScriptStatus_ : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
enum EntityScriptStatus {
|
||||
PENDING,
|
||||
LOADING,
|
||||
ERROR_LOADING_SCRIPT,
|
||||
ERROR_RUNNING_SCRIPT,
|
||||
RUNNING,
|
||||
UNLOADED
|
||||
};
|
||||
Q_ENUM(EntityScriptStatus)
|
||||
static QString valueToKey(EntityScriptStatus status) {
|
||||
return QMetaEnum::fromType<EntityScriptStatus>().valueToKey(status);
|
||||
}
|
||||
};
|
||||
|
||||
using EntityScriptStatus = EntityScriptStatus_::EntityScriptStatus;
|
||||
#endif // hifi_EntityScriptUtils_h
|
|
@ -38,6 +38,7 @@ public:
|
|||
InvalidURL,
|
||||
NotFound
|
||||
};
|
||||
Q_ENUM(Result)
|
||||
|
||||
QByteArray getData() { return _data; }
|
||||
State getState() const { return _state; }
|
||||
|
|
|
@ -27,11 +27,12 @@ BatchLoader::BatchLoader(const QList<QUrl>& urls)
|
|||
_started(false),
|
||||
_finished(false),
|
||||
_urls(urls.toSet()),
|
||||
_data() {
|
||||
_data(),
|
||||
_status() {
|
||||
qRegisterMetaType<QMap<QUrl, QString>>("QMap<QUrl, QString>");
|
||||
}
|
||||
|
||||
void BatchLoader::start() {
|
||||
void BatchLoader::start(int maxRetries) {
|
||||
if (_started) {
|
||||
return;
|
||||
}
|
||||
|
@ -40,7 +41,7 @@ void BatchLoader::start() {
|
|||
|
||||
if (_urls.size() == 0) {
|
||||
_finished = true;
|
||||
emit finished(_data);
|
||||
emit finished(_data, _status);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -58,7 +59,8 @@ void BatchLoader::start() {
|
|||
ScriptCacheSignalProxy* proxy = new ScriptCacheSignalProxy();
|
||||
connect(scriptCache.data(), &ScriptCache::destroyed, proxy, &ScriptCacheSignalProxy::deleteLater);
|
||||
|
||||
connect(proxy, &ScriptCacheSignalProxy::contentAvailable, this, [this](const QString& url, const QString& contents, bool isURL, bool success) {
|
||||
connect(proxy, &ScriptCacheSignalProxy::contentAvailable, this, [this](const QString& url, const QString& contents, bool isURL, bool success, const QString& status) {
|
||||
_status.insert(url, status);
|
||||
if (isURL && success) {
|
||||
_data.insert(url, contents);
|
||||
qCDebug(scriptengine) << "Loaded: " << url;
|
||||
|
@ -69,17 +71,17 @@ void BatchLoader::start() {
|
|||
|
||||
if (!_finished && _urls.size() == _data.size()) {
|
||||
_finished = true;
|
||||
emit finished(_data);
|
||||
emit finished(_data, _status);
|
||||
}
|
||||
});
|
||||
|
||||
scriptCache->getScriptContents(url.toString(), [proxy](const QString& url, const QString& contents, bool isURL, bool success) {
|
||||
proxy->receivedContent(url, contents, isURL, success);
|
||||
scriptCache->getScriptContents(url.toString(), [proxy](const QString& url, const QString& contents, bool isURL, bool success, const QString& status) {
|
||||
proxy->receivedContent(url, contents, isURL, success, status);
|
||||
proxy->deleteLater();
|
||||
}, false);
|
||||
}, false, maxRetries);
|
||||
}
|
||||
}
|
||||
|
||||
void ScriptCacheSignalProxy::receivedContent(const QString& url, const QString& contents, bool isURL, bool success) {
|
||||
emit contentAvailable(url, contents, isURL, success);
|
||||
void ScriptCacheSignalProxy::receivedContent(const QString& url, const QString& contents, bool isURL, bool success, const QString& status) {
|
||||
emit contentAvailable(url, contents, isURL, success, status);
|
||||
}
|
||||
|
|
|
@ -19,15 +19,17 @@
|
|||
#include <QString>
|
||||
#include <QUrl>
|
||||
|
||||
#include "ScriptCache.h"
|
||||
|
||||
#include <mutex>
|
||||
|
||||
class ScriptCacheSignalProxy : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
void receivedContent(const QString& url, const QString& contents, bool isURL, bool success);
|
||||
void receivedContent(const QString& url, const QString& contents, bool isURL, bool success, const QString& status);
|
||||
|
||||
signals:
|
||||
void contentAvailable(const QString& url, const QString& contents, bool isURL, bool success);
|
||||
void contentAvailable(const QString& url, const QString& contents, bool isURL, bool success, const QString& status);
|
||||
};
|
||||
|
||||
class BatchLoader : public QObject {
|
||||
|
@ -35,11 +37,11 @@ class BatchLoader : public QObject {
|
|||
public:
|
||||
BatchLoader(const QList<QUrl>& urls);
|
||||
|
||||
void start();
|
||||
void start(int maxRetries = ScriptRequest::MAX_RETRIES);
|
||||
bool isFinished() const { return _finished; };
|
||||
|
||||
signals:
|
||||
void finished(const QMap<QUrl, QString>& data);
|
||||
void finished(const QMap<QUrl, QString>& data, const QMap<QUrl, QString>& status);
|
||||
|
||||
private:
|
||||
void checkFinished();
|
||||
|
@ -48,6 +50,7 @@ private:
|
|||
bool _finished;
|
||||
QSet<QUrl> _urls;
|
||||
QMap<QUrl, QString> _data;
|
||||
QMap<QUrl, QString> _status;
|
||||
};
|
||||
|
||||
#endif // hifi_BatchLoader_h
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
#include <QObject>
|
||||
#include <QThread>
|
||||
#include <QRegularExpression>
|
||||
#include <QMetaEnum>
|
||||
|
||||
#include <assert.h>
|
||||
#include <SharedUtil.h>
|
||||
|
@ -27,12 +28,18 @@
|
|||
#include "ScriptEngineLogging.h"
|
||||
#include <QtCore/QTimer>
|
||||
|
||||
const QString ScriptCache::STATUS_INLINE { "Inline" };
|
||||
const QString ScriptCache::STATUS_CACHED { "Cached" };
|
||||
|
||||
ScriptCache::ScriptCache(QObject* parent) {
|
||||
// nothing to do here...
|
||||
}
|
||||
|
||||
void ScriptCache::clearCache() {
|
||||
Lock lock(_containerLock);
|
||||
foreach(auto& url, _scriptCache.keys()) {
|
||||
qCDebug(scriptengine) << "clearing cache: " << url;
|
||||
}
|
||||
_scriptCache.clear();
|
||||
}
|
||||
|
||||
|
@ -49,35 +56,6 @@ void ScriptCache::clearATPScriptsFromCache() {
|
|||
}
|
||||
}
|
||||
|
||||
QString ScriptCache::getScript(const QUrl& unnormalizedURL, ScriptUser* scriptUser, bool& isPending, bool reload) {
|
||||
QUrl url = ResourceManager::normalizeURL(unnormalizedURL);
|
||||
QString scriptContents;
|
||||
|
||||
Lock lock(_containerLock);
|
||||
if (_scriptCache.contains(url) && !reload) {
|
||||
qCDebug(scriptengine) << "Found script in cache:" << url.toString();
|
||||
scriptContents = _scriptCache[url];
|
||||
lock.unlock();
|
||||
scriptUser->scriptContentsAvailable(url, scriptContents);
|
||||
isPending = false;
|
||||
} else {
|
||||
isPending = true;
|
||||
bool alreadyWaiting = _scriptUsers.contains(url);
|
||||
_scriptUsers.insert(url, scriptUser);
|
||||
lock.unlock();
|
||||
|
||||
if (alreadyWaiting) {
|
||||
qCDebug(scriptengine) << "Already downloading script at:" << url.toString();
|
||||
} else {
|
||||
auto request = ResourceManager::createResourceRequest(nullptr, url);
|
||||
request->setCacheEnabled(!reload);
|
||||
connect(request, &ResourceRequest::finished, this, &ScriptCache::scriptDownloaded);
|
||||
request->send();
|
||||
}
|
||||
}
|
||||
return scriptContents;
|
||||
}
|
||||
|
||||
void ScriptCache::deleteScript(const QUrl& unnormalizedURL) {
|
||||
QUrl url = ResourceManager::normalizeURL(unnormalizedURL);
|
||||
Lock lock(_containerLock);
|
||||
|
@ -87,37 +65,7 @@ void ScriptCache::deleteScript(const QUrl& unnormalizedURL) {
|
|||
}
|
||||
}
|
||||
|
||||
void ScriptCache::scriptDownloaded() {
|
||||
ResourceRequest* req = qobject_cast<ResourceRequest*>(sender());
|
||||
QUrl url = req->getUrl();
|
||||
|
||||
Lock lock(_containerLock);
|
||||
QList<ScriptUser*> scriptUsers = _scriptUsers.values(url);
|
||||
_scriptUsers.remove(url);
|
||||
|
||||
if (!DependencyManager::get<ScriptEngines>()->isStopped()) {
|
||||
if (req->getResult() == ResourceRequest::Success) {
|
||||
auto scriptContents = req->getData();
|
||||
_scriptCache[url] = scriptContents;
|
||||
lock.unlock();
|
||||
qCDebug(scriptengine) << "Done downloading script at:" << url.toString();
|
||||
|
||||
foreach(ScriptUser* user, scriptUsers) {
|
||||
user->scriptContentsAvailable(url, scriptContents);
|
||||
}
|
||||
} else {
|
||||
lock.unlock();
|
||||
qCWarning(scriptengine) << "Error loading script from URL " << url;
|
||||
foreach(ScriptUser* user, scriptUsers) {
|
||||
user->errorInLoadingScript(url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
req->deleteLater();
|
||||
}
|
||||
|
||||
void ScriptCache::getScriptContents(const QString& scriptOrURL, contentAvailableCallback contentAvailable, bool forceDownload) {
|
||||
void ScriptCache::getScriptContents(const QString& scriptOrURL, contentAvailableCallback contentAvailable, bool forceDownload, int maxRetries) {
|
||||
#ifdef THREAD_DEBUGGING
|
||||
qCDebug(scriptengine) << "ScriptCache::getScriptContents() on thread [" << QThread::currentThread() << "] expected thread [" << thread() << "]";
|
||||
#endif
|
||||
|
@ -128,7 +76,7 @@ void ScriptCache::getScriptContents(const QString& scriptOrURL, contentAvailable
|
|||
// entityScript use case)
|
||||
if (unnormalizedURL.scheme().isEmpty() &&
|
||||
scriptOrURL.simplified().replace(" ", "").contains(QRegularExpression(R"(\(function\([a-z]?[\w,]*\){)"))) {
|
||||
contentAvailable(scriptOrURL, scriptOrURL, false, true);
|
||||
contentAvailable(scriptOrURL, scriptOrURL, false, true, STATUS_INLINE);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -136,7 +84,7 @@ void ScriptCache::getScriptContents(const QString& scriptOrURL, contentAvailable
|
|||
if (unnormalizedURL.scheme() == "javascript") {
|
||||
QString contents { scriptOrURL };
|
||||
contents.replace(QRegularExpression("^javascript:"), "");
|
||||
contentAvailable(scriptOrURL, contents, false, true);
|
||||
contentAvailable(scriptOrURL, contents, false, true, STATUS_INLINE);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -145,34 +93,32 @@ void ScriptCache::getScriptContents(const QString& scriptOrURL, contentAvailable
|
|||
auto scriptContent = _scriptCache[url];
|
||||
lock.unlock();
|
||||
qCDebug(scriptengine) << "Found script in cache:" << url.toString();
|
||||
contentAvailable(url.toString(), scriptContent, true, true);
|
||||
contentAvailable(url.toString(), scriptContent, true, true, STATUS_CACHED);
|
||||
} else {
|
||||
auto& scriptRequest = _activeScriptRequests[url];
|
||||
|
||||
bool alreadyWaiting = scriptRequest.scriptUsers.size() > 0;
|
||||
scriptRequest.scriptUsers.push_back(contentAvailable);
|
||||
|
||||
lock.unlock();
|
||||
|
||||
if (alreadyWaiting) {
|
||||
qCDebug(scriptengine) << "Already downloading script at:" << url.toString();
|
||||
qCDebug(scriptengine) << QString("Already downloading script at: %1 (retry: %2; scriptusers: %3)")
|
||||
.arg(url.toString()).arg(scriptRequest.numRetries).arg(scriptRequest.scriptUsers.size());
|
||||
} else {
|
||||
scriptRequest.maxRetries = maxRetries;
|
||||
#ifdef THREAD_DEBUGGING
|
||||
qCDebug(scriptengine) << "about to call: ResourceManager::createResourceRequest(this, url); on thread [" << QThread::currentThread() << "] expected thread [" << thread() << "]";
|
||||
#endif
|
||||
auto request = ResourceManager::createResourceRequest(nullptr, url);
|
||||
Q_ASSERT(request);
|
||||
request->setCacheEnabled(!forceDownload);
|
||||
connect(request, &ResourceRequest::finished, this, &ScriptCache::scriptContentAvailable);
|
||||
connect(request, &ResourceRequest::finished, this, [=]{ scriptContentAvailable(maxRetries); });
|
||||
request->send();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static const int MAX_RETRIES = 5;
|
||||
static int START_DELAY_BETWEEN_RETRIES = 200;
|
||||
|
||||
void ScriptCache::scriptContentAvailable() {
|
||||
void ScriptCache::scriptContentAvailable(int maxRetries) {
|
||||
#ifdef THREAD_DEBUGGING
|
||||
qCDebug(scriptengine) << "ScriptCache::scriptContentAvailable() on thread [" << QThread::currentThread() << "] expected thread [" << thread() << "]";
|
||||
#endif
|
||||
|
@ -181,7 +127,7 @@ void ScriptCache::scriptContentAvailable() {
|
|||
|
||||
QString scriptContent;
|
||||
std::vector<contentAvailableCallback> allCallbacks;
|
||||
bool finished { false };
|
||||
QString status = QMetaEnum::fromType<ResourceRequest::Result>().valueToKey(req->getResult());
|
||||
bool success { false };
|
||||
|
||||
{
|
||||
|
@ -199,7 +145,6 @@ void ScriptCache::scriptContentAvailable() {
|
|||
_activeScriptRequests.remove(url);
|
||||
|
||||
_scriptCache[url] = scriptContent = req->getData();
|
||||
finished = true;
|
||||
qCDebug(scriptengine) << "Done downloading script at:" << url.toString();
|
||||
} else {
|
||||
auto result = req->getResult();
|
||||
|
@ -207,16 +152,19 @@ void ScriptCache::scriptContentAvailable() {
|
|||
result == ResourceRequest::AccessDenied ||
|
||||
result == ResourceRequest::InvalidURL ||
|
||||
result == ResourceRequest::NotFound ||
|
||||
scriptRequest.numRetries >= MAX_RETRIES;
|
||||
scriptRequest.numRetries >= maxRetries;
|
||||
|
||||
if (!irrecoverable) {
|
||||
++scriptRequest.numRetries;
|
||||
|
||||
qCDebug(scriptengine) << "Script request failed: " << url;
|
||||
int timeout = exp(scriptRequest.numRetries) * ScriptRequest::START_DELAY_BETWEEN_RETRIES;
|
||||
int attempt = scriptRequest.numRetries;
|
||||
qCDebug(scriptengine) << QString("Script request failed [%1]: %2 (will retry %3 more times; attempt #%4 in %5ms...)")
|
||||
.arg(status).arg(url.toString()).arg(maxRetries - attempt + 1).arg(attempt).arg(timeout);
|
||||
|
||||
int timeout = exp(scriptRequest.numRetries) * START_DELAY_BETWEEN_RETRIES;
|
||||
QTimer::singleShot(timeout, this, [this, url]() {
|
||||
qCDebug(scriptengine) << "Retrying script request: " << url;
|
||||
QTimer::singleShot(timeout, this, [this, url, attempt, maxRetries]() {
|
||||
qCDebug(scriptengine) << QString("Retrying script request [%1 / %2]: %3")
|
||||
.arg(attempt).arg(maxRetries).arg(url.toString());
|
||||
|
||||
auto request = ResourceManager::createResourceRequest(nullptr, url);
|
||||
Q_ASSERT(request);
|
||||
|
@ -224,7 +172,7 @@ void ScriptCache::scriptContentAvailable() {
|
|||
// We've already made a request, so the cache must be disabled or it wasn't there, so enabling
|
||||
// it will do nothing.
|
||||
request->setCacheEnabled(false);
|
||||
connect(request, &ResourceRequest::finished, this, &ScriptCache::scriptContentAvailable);
|
||||
connect(request, &ResourceRequest::finished, this, [=]{ scriptContentAvailable(maxRetries); });
|
||||
request->send();
|
||||
});
|
||||
} else {
|
||||
|
@ -232,9 +180,12 @@ void ScriptCache::scriptContentAvailable() {
|
|||
|
||||
allCallbacks = scriptRequest.scriptUsers;
|
||||
|
||||
scriptContent = _scriptCache[url];
|
||||
finished = true;
|
||||
qCWarning(scriptengine) << "Error loading script from URL " << url;
|
||||
if (_scriptCache.contains(url)) {
|
||||
scriptContent = _scriptCache[url];
|
||||
}
|
||||
_activeScriptRequests.remove(url);
|
||||
qCWarning(scriptengine) << "Error loading script from URL " << url << "(" << status <<")";
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -242,9 +193,9 @@ void ScriptCache::scriptContentAvailable() {
|
|||
|
||||
req->deleteLater();
|
||||
|
||||
if (finished && !DependencyManager::get<ScriptEngines>()->isStopped()) {
|
||||
if (allCallbacks.size() > 0 && !DependencyManager::get<ScriptEngines>()->isStopped()) {
|
||||
foreach(contentAvailableCallback thisCallback, allCallbacks) {
|
||||
thisCallback(url.toString(), scriptContent, true, success);
|
||||
thisCallback(url.toString(), scriptContent, true, success, status);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
#include <mutex>
|
||||
#include <ResourceCache.h>
|
||||
|
||||
using contentAvailableCallback = std::function<void(const QString& scriptOrURL, const QString& contents, bool isURL, bool contentAvailable)>;
|
||||
using contentAvailableCallback = std::function<void(const QString& scriptOrURL, const QString& contents, bool isURL, bool contentAvailable, const QString& status)>;
|
||||
|
||||
class ScriptUser {
|
||||
public:
|
||||
|
@ -25,8 +25,11 @@ public:
|
|||
|
||||
class ScriptRequest {
|
||||
public:
|
||||
static const int MAX_RETRIES { 5 };
|
||||
static const int START_DELAY_BETWEEN_RETRIES { 200 };
|
||||
std::vector<contentAvailableCallback> scriptUsers { };
|
||||
int numRetries { 0 };
|
||||
int maxRetries { MAX_RETRIES };
|
||||
};
|
||||
|
||||
/// Interface for loading scripts
|
||||
|
@ -38,23 +41,17 @@ class ScriptCache : public QObject, public Dependency {
|
|||
using Lock = std::unique_lock<Mutex>;
|
||||
|
||||
public:
|
||||
static const QString STATUS_INLINE;
|
||||
static const QString STATUS_CACHED;
|
||||
|
||||
void clearCache();
|
||||
Q_INVOKABLE void clearATPScriptsFromCache();
|
||||
void getScriptContents(const QString& scriptOrURL, contentAvailableCallback contentAvailable, bool forceDownload = false);
|
||||
void getScriptContents(const QString& scriptOrURL, contentAvailableCallback contentAvailable, bool forceDownload = false, int maxRetries = ScriptRequest::MAX_RETRIES);
|
||||
|
||||
|
||||
QString getScript(const QUrl& unnormalizedURL, ScriptUser* scriptUser, bool& isPending, bool redownload = false);
|
||||
void deleteScript(const QUrl& unnormalizedURL);
|
||||
|
||||
// FIXME - how do we remove a script from the bad script list in the case of a redownload?
|
||||
void addScriptToBadScriptList(const QUrl& url) { _badScripts.insert(url); }
|
||||
bool isInBadScriptList(const QUrl& url) { return _badScripts.contains(url); }
|
||||
|
||||
private slots:
|
||||
void scriptDownloaded(); // old version
|
||||
void scriptContentAvailable(); // new version
|
||||
|
||||
private:
|
||||
void scriptContentAvailable(int maxRetries); // new version
|
||||
ScriptCache(QObject* parent = NULL);
|
||||
|
||||
Mutex _containerLock;
|
||||
|
@ -62,7 +59,6 @@ private:
|
|||
|
||||
QHash<QUrl, QString> _scriptCache;
|
||||
QMultiMap<QUrl, ScriptUser*> _scriptUsers;
|
||||
QSet<QUrl> _badScripts;
|
||||
};
|
||||
|
||||
#endif // hifi_ScriptCache_h
|
||||
|
|
|
@ -68,10 +68,12 @@
|
|||
|
||||
#include "MIDIEvent.h"
|
||||
|
||||
static const QString SCRIPT_EXCEPTION_FORMAT = "[UncaughtException] %1 in %2:%3";
|
||||
const QString BaseScriptEngine::SCRIPT_EXCEPTION_FORMAT { "[UncaughtException] %1 in %2:%3" };
|
||||
static const QScriptEngine::QObjectWrapOptions DEFAULT_QOBJECT_WRAP_OPTIONS =
|
||||
QScriptEngine::ExcludeDeleteLater | QScriptEngine::ExcludeChildObjects;
|
||||
|
||||
static const bool HIFI_AUTOREFRESH_FILE_SCRIPTS { true };
|
||||
|
||||
Q_DECLARE_METATYPE(QScriptEngine::FunctionSignature)
|
||||
int functionSignatureMetaID = qRegisterMetaType<QScriptEngine::FunctionSignature>();
|
||||
|
||||
|
@ -137,40 +139,51 @@ QString encodeEntityIdIntoEntityUrl(const QString& url, const QString& entityID)
|
|||
return url + " [EntityID:" + entityID + "]";
|
||||
}
|
||||
|
||||
static bool hasCorrectSyntax(const QScriptProgram& program, ScriptEngine* reportingEngine) {
|
||||
const auto syntaxCheck = QScriptEngine::checkSyntax(program.sourceCode());
|
||||
if (syntaxCheck.state() != QScriptSyntaxCheckResult::Valid) {
|
||||
QString BaseScriptEngine::lintScript(const QString& sourceCode, const QString& fileName, const int lineNumber) {
|
||||
const auto syntaxCheck = checkSyntax(sourceCode);
|
||||
if (syntaxCheck.state() != syntaxCheck.Valid) {
|
||||
const auto error = syntaxCheck.errorMessage();
|
||||
const auto line = QString::number(syntaxCheck.errorLineNumber());
|
||||
const auto column = QString::number(syntaxCheck.errorColumnNumber());
|
||||
const auto message = QString("[SyntaxError] %1 in %2:%3(%4)").arg(error, program.fileName(), line, column);
|
||||
reportingEngine->scriptErrorMessage(qPrintable(message));
|
||||
return false;
|
||||
const auto message = QString("[SyntaxError] %1 in %2:%3(%4)").arg(error, fileName, line, column);
|
||||
return message;
|
||||
}
|
||||
return true;
|
||||
return QString();
|
||||
}
|
||||
|
||||
static bool hadUncaughtExceptions(QScriptEngine& engine, const QString& fileName, ScriptEngine* reportingEngine, QString* exceptionMessage = nullptr) {
|
||||
if (engine.hasUncaughtException()) {
|
||||
const auto backtrace = engine.uncaughtExceptionBacktrace();
|
||||
const auto exception = engine.uncaughtException().toString();
|
||||
const auto line = QString::number(engine.uncaughtExceptionLineNumber());
|
||||
engine.clearExceptions();
|
||||
QString BaseScriptEngine::formatUncaughtException(const QString& overrideFileName) {
|
||||
QString message;
|
||||
if (hasUncaughtException()) {
|
||||
const auto error = uncaughtException();
|
||||
const auto backtrace = uncaughtExceptionBacktrace();
|
||||
const auto exception = error.toString();
|
||||
auto filename = overrideFileName;
|
||||
if (filename.isEmpty()) {
|
||||
QScriptContextInfo ctx { currentContext() };
|
||||
filename = ctx.fileName();
|
||||
}
|
||||
const auto line = QString::number(uncaughtExceptionLineNumber());
|
||||
|
||||
QString message = QString(SCRIPT_EXCEPTION_FORMAT).arg(exception, fileName, line);
|
||||
message = QString(SCRIPT_EXCEPTION_FORMAT).arg(exception, overrideFileName, line);
|
||||
if (!backtrace.empty()) {
|
||||
static const auto lineSeparator = "\n ";
|
||||
message += QString("\n[Backtrace]%1%2").arg(lineSeparator, backtrace.join(lineSeparator));
|
||||
}
|
||||
reportingEngine->scriptErrorMessage(qPrintable(message));
|
||||
if (exceptionMessage) {
|
||||
*exceptionMessage = message;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
return message;
|
||||
}
|
||||
|
||||
QString ScriptEngine::reportUncaughtException(const QString& overrideFileName) {
|
||||
QString message;
|
||||
if (!hasUncaughtException()) {
|
||||
return message;
|
||||
}
|
||||
message = formatUncaughtException(overrideFileName.isEmpty() ? _fileNameString : overrideFileName);
|
||||
scriptErrorMessage(qPrintable(message));
|
||||
return message;
|
||||
}
|
||||
|
||||
int ScriptEngine::processLevelMaxRetries { ScriptRequest::MAX_RETRIES };
|
||||
ScriptEngine::ScriptEngine(Context context, const QString& scriptContents, const QString& fileNameString) :
|
||||
_context(context),
|
||||
_scriptContents(scriptContents),
|
||||
|
@ -181,10 +194,16 @@ ScriptEngine::ScriptEngine(Context context, const QString& scriptContents, const
|
|||
DependencyManager::get<ScriptEngines>()->addScriptEngine(this);
|
||||
|
||||
connect(this, &QScriptEngine::signalHandlerException, this, [this](const QScriptValue& exception) {
|
||||
hadUncaughtExceptions(*this, _fileNameString, this);
|
||||
reportUncaughtException();
|
||||
clearExceptions();
|
||||
});
|
||||
|
||||
setProcessEventsInterval(MSECS_PER_SECOND);
|
||||
if (isEntityServerScript()) {
|
||||
qCDebug(scriptengine) << "isEntityServerScript() -- limiting maxRetries to 1";
|
||||
processLevelMaxRetries = 1;
|
||||
}
|
||||
qCDebug(scriptengine) << getContext() << "processLevelMaxRetries =" << processLevelMaxRetries;
|
||||
}
|
||||
|
||||
QString ScriptEngine::getContext() const {
|
||||
|
@ -301,7 +320,10 @@ void ScriptEngine::runDebuggable() {
|
|||
}
|
||||
_lastUpdate = now;
|
||||
// Debug and clear exceptions
|
||||
hadUncaughtExceptions(*this, _fileNameString, this);
|
||||
if (hasUncaughtException()) {
|
||||
reportUncaughtException();
|
||||
clearExceptions();
|
||||
}
|
||||
});
|
||||
|
||||
timer->start(10);
|
||||
|
@ -334,6 +356,16 @@ void ScriptEngine::runInThread() {
|
|||
workerThread->start();
|
||||
}
|
||||
|
||||
void ScriptEngine::executeOnScriptThread(std::function<void()> function, bool blocking ) {
|
||||
if (QThread::currentThread() != thread()) {
|
||||
QMetaObject::invokeMethod(this, "executeOnScriptThread", blocking ? Qt::BlockingQueuedConnection : Qt::QueuedConnection,
|
||||
Q_ARG(std::function<void()>, function));
|
||||
return;
|
||||
}
|
||||
|
||||
function();
|
||||
}
|
||||
|
||||
void ScriptEngine::waitTillDoneRunning() {
|
||||
auto workerThread = thread();
|
||||
|
||||
|
@ -399,8 +431,6 @@ QString ScriptEngine::getFilename() const {
|
|||
return lastPart;
|
||||
}
|
||||
|
||||
|
||||
// FIXME - switch this to the new model of ScriptCache callbacks
|
||||
void ScriptEngine::loadURL(const QUrl& scriptURL, bool reload) {
|
||||
if (_isRunning) {
|
||||
return;
|
||||
|
@ -410,19 +440,27 @@ void ScriptEngine::loadURL(const QUrl& scriptURL, bool reload) {
|
|||
_fileNameString = url.toString();
|
||||
_isReloading = reload;
|
||||
|
||||
bool isPending;
|
||||
const auto maxRetries = 0; // for consistency with previous scriptCache->getScript() behavior
|
||||
auto scriptCache = DependencyManager::get<ScriptCache>();
|
||||
scriptCache->getScript(url, this, isPending, reload);
|
||||
}
|
||||
scriptCache->getScriptContents(url.toString(), [this](const QString& url, const QString& scriptContents, bool isURL, bool success, const QString&status) {
|
||||
qCDebug(scriptengine) << "loadURL" << url << status << QThread::currentThread();
|
||||
if (!success) {
|
||||
scriptErrorMessage("ERROR Loading file (" + status + "):" + url);
|
||||
emit errorLoadingScript(_fileNameString);
|
||||
return;
|
||||
}
|
||||
|
||||
// FIXME - switch this to the new model of ScriptCache callbacks
|
||||
void ScriptEngine::scriptContentsAvailable(const QUrl& url, const QString& scriptContents) {
|
||||
_scriptContents = scriptContents;
|
||||
static const QString DEBUG_FLAG("#debug");
|
||||
if (QRegularExpression(DEBUG_FLAG).match(scriptContents).hasMatch()) {
|
||||
_debuggable = true;
|
||||
}
|
||||
emit scriptLoaded(url.toString());
|
||||
_scriptContents = scriptContents;
|
||||
|
||||
{
|
||||
static const QString DEBUG_FLAG("#debug");
|
||||
if (QRegularExpression(DEBUG_FLAG).match(scriptContents).hasMatch()) {
|
||||
qCWarning(scriptengine) << "NOTE: ScriptEngine for " << QUrl(url).fileName() << " will be launched in debug mode";
|
||||
_debuggable = true;
|
||||
}
|
||||
}
|
||||
emit scriptLoaded(url);
|
||||
}, reload, maxRetries);
|
||||
}
|
||||
|
||||
void ScriptEngine::scriptErrorMessage(const QString& message) {
|
||||
|
@ -440,12 +478,6 @@ void ScriptEngine::scriptInfoMessage(const QString& message) {
|
|||
emit infoMessage(message);
|
||||
}
|
||||
|
||||
// FIXME - switch this to the new model of ScriptCache callbacks
|
||||
void ScriptEngine::errorInLoadingScript(const QUrl& url) {
|
||||
scriptErrorMessage("ERROR Loading file:" + url.toString());
|
||||
emit errorLoadingScript(_fileNameString);
|
||||
}
|
||||
|
||||
// Even though we never pass AnimVariantMap directly to and from javascript, the queued invokeMethod of
|
||||
// callAnimationStateHandler requires that the type be registered.
|
||||
// These two are meaningful, if we ever do want to use them...
|
||||
|
@ -520,6 +552,15 @@ void ScriptEngine::init() {
|
|||
|
||||
auto entityScriptingInterface = DependencyManager::get<EntityScriptingInterface>();
|
||||
entityScriptingInterface->init();
|
||||
connect(entityScriptingInterface.data(), &EntityScriptingInterface::deletingEntity, this, [this](const EntityItemID& entityID) {
|
||||
if (_entityScripts.contains(entityID)) {
|
||||
if (isEntityScriptRunning(entityID)) {
|
||||
qCWarning(scriptengine) << "deletingEntity while entity script is still running!" << entityID;
|
||||
}
|
||||
_entityScripts.remove(entityID);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// register various meta-types
|
||||
registerMetaTypes(this);
|
||||
|
@ -850,17 +891,25 @@ QScriptValue ScriptEngine::evaluate(const QString& sourceCode, const QString& fi
|
|||
}
|
||||
|
||||
// Check syntax
|
||||
const QScriptProgram program(sourceCode, fileName, lineNumber);
|
||||
if (!hasCorrectSyntax(program, this)) {
|
||||
auto syntaxError = lintScript(sourceCode, fileName);
|
||||
QScriptProgram program { sourceCode, fileName, lineNumber };
|
||||
if (!syntaxError.isEmpty() || program.isNull()) {
|
||||
scriptErrorMessage(qPrintable(syntaxError));
|
||||
return QScriptValue();
|
||||
}
|
||||
|
||||
++_evaluatesPending;
|
||||
const auto result = QScriptEngine::evaluate(program);
|
||||
auto result = BaseScriptEngine::evaluate(program);
|
||||
--_evaluatesPending;
|
||||
|
||||
const auto hadUncaughtException = hadUncaughtExceptions(*this, program.fileName(), this);
|
||||
emit evaluationFinished(result, hadUncaughtException);
|
||||
if (hasUncaughtException()) {
|
||||
result = uncaughtException();
|
||||
reportUncaughtException(program.fileName());
|
||||
emit evaluationFinished(result, true);
|
||||
clearExceptions();
|
||||
} else {
|
||||
emit evaluationFinished(result, false);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
@ -1009,7 +1058,10 @@ void ScriptEngine::run() {
|
|||
_lastUpdate = now;
|
||||
|
||||
// Debug and clear exceptions
|
||||
hadUncaughtExceptions(*this, _fileNameString, this);
|
||||
if (hasUncaughtException()) {
|
||||
reportUncaughtException();
|
||||
clearExceptions();
|
||||
}
|
||||
}
|
||||
|
||||
scriptInfoMessage("Script Engine stopping:" + getFilename());
|
||||
|
@ -1299,12 +1351,12 @@ void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callbac
|
|||
EntityItemID capturedEntityIdentifier = currentEntityIdentifier;
|
||||
QUrl capturedSandboxURL = currentSandboxURL;
|
||||
|
||||
auto evaluateScripts = [=](const QMap<QUrl, QString>& data) {
|
||||
auto evaluateScripts = [=](const QMap<QUrl, QString>& data, const QMap<QUrl, QString>& status) {
|
||||
auto parentURL = _parentURL;
|
||||
for (QUrl url : urls) {
|
||||
QString contents = data[url];
|
||||
if (contents.isNull()) {
|
||||
scriptErrorMessage("Error loading file: " + url.toString());
|
||||
scriptErrorMessage("Error loading file (" + status[url] +"): " + url.toString());
|
||||
} else {
|
||||
std::lock_guard<std::recursive_mutex> lock(_lock);
|
||||
if (!_includedURLs.contains(url)) {
|
||||
|
@ -1336,7 +1388,7 @@ void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callbac
|
|||
// If we are destroyed before the loader completes, make sure to clean it up
|
||||
connect(this, &QObject::destroyed, loader, &QObject::deleteLater);
|
||||
|
||||
loader->start();
|
||||
loader->start(processLevelMaxRetries);
|
||||
|
||||
if (!callback.isFunction() && !loader->isFinished()) {
|
||||
QEventLoop loop;
|
||||
|
@ -1368,7 +1420,7 @@ void ScriptEngine::load(const QString& loadFile) {
|
|||
}
|
||||
if (!currentEntityIdentifier.isInvalidID()) {
|
||||
scriptWarningMessage("Script.load() from entity script is ignored... loadFile:"
|
||||
+ loadFile + "parent script:" + getFilename());
|
||||
+ loadFile + "parent script:" + getFilename() + "entity: " + currentEntityIdentifier.toString());
|
||||
return; // bail early
|
||||
}
|
||||
|
||||
|
@ -1411,13 +1463,19 @@ void ScriptEngine::forwardHandlerCall(const EntityItemID& entityID, const QStrin
|
|||
int ScriptEngine::getNumRunningEntityScripts() const {
|
||||
int sum = 0;
|
||||
for (auto& st : _entityScripts) {
|
||||
if (st.status == RUNNING) {
|
||||
if (st.status == EntityScriptStatus::RUNNING) {
|
||||
++sum;
|
||||
}
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
QString ScriptEngine::getEntityScriptStatus(const EntityItemID& entityID) {
|
||||
if (_entityScripts.contains(entityID))
|
||||
return EntityScriptStatus_::valueToKey(_entityScripts[entityID].status).toLower();
|
||||
return QString();
|
||||
}
|
||||
|
||||
bool ScriptEngine::getEntityScriptDetails(const EntityItemID& entityID, EntityScriptDetails &details) const {
|
||||
auto it = _entityScripts.constFind(entityID);
|
||||
if (it == _entityScripts.constEnd()) {
|
||||
|
@ -1427,28 +1485,48 @@ bool ScriptEngine::getEntityScriptDetails(const EntityItemID& entityID, EntitySc
|
|||
return true;
|
||||
}
|
||||
|
||||
void ScriptEngine::setEntityScriptDetails(const EntityItemID& entityID, const EntityScriptDetails& details) {
|
||||
_entityScripts[entityID] = details;
|
||||
emit entityScriptDetailsUpdated();
|
||||
}
|
||||
|
||||
void ScriptEngine::updateEntityScriptStatus(const EntityItemID& entityID, const EntityScriptStatus &status, const QString& errorInfo) {
|
||||
EntityScriptDetails &details = _entityScripts[entityID];
|
||||
details.status = status;
|
||||
details.errorInfo = errorInfo;
|
||||
emit entityScriptDetailsUpdated();
|
||||
}
|
||||
|
||||
// since all of these operations can be asynch we will always do the actual work in the response handler
|
||||
// for the download
|
||||
void ScriptEngine::loadEntityScript(QWeakPointer<ScriptEngine> theEngine, const EntityItemID& entityID, const QString& entityScript, bool forceRedownload) {
|
||||
auto engine = theEngine.data();
|
||||
engine->executeOnScriptThread([=]{
|
||||
EntityScriptDetails details = engine->_entityScripts[entityID];
|
||||
if (details.status == EntityScriptStatus::PENDING || details.status == EntityScriptStatus::UNLOADED) {
|
||||
engine->updateEntityScriptStatus(entityID, EntityScriptStatus::LOADING, QThread::currentThread()->objectName());
|
||||
}
|
||||
});
|
||||
|
||||
// NOTE: If the script content is not currently in the cache, the LAMBDA here will be called on the Main Thread
|
||||
// which means we're guaranteed that it's not the correct thread for the ScriptEngine. This means
|
||||
// when we get into entityScriptContentAvailable() we will likely invokeMethod() to get it over
|
||||
// to the "Entities" ScriptEngine thread.
|
||||
DependencyManager::get<ScriptCache>()->getScriptContents(entityScript, [theEngine, entityID](const QString& scriptOrURL, const QString& contents, bool isURL, bool success) {
|
||||
DependencyManager::get<ScriptCache>()->getScriptContents(entityScript, [theEngine, entityID](const QString& scriptOrURL, const QString& contents, bool isURL, bool success, const QString &status) {
|
||||
QSharedPointer<ScriptEngine> strongEngine = theEngine.toStrongRef();
|
||||
if (strongEngine) {
|
||||
#ifdef THREAD_DEBUGGING
|
||||
qCDebug(scriptengine) << "ScriptEngine::entityScriptContentAvailable() IN LAMBDA contentAvailable on thread ["
|
||||
<< QThread::currentThread() << "] expected thread [" << strongEngine->thread() << "]";
|
||||
#endif
|
||||
strongEngine->entityScriptContentAvailable(entityID, scriptOrURL, contents, isURL, success);
|
||||
strongEngine->entityScriptContentAvailable(entityID, scriptOrURL, contents, isURL, success, status);
|
||||
}
|
||||
}, forceRedownload);
|
||||
}, forceRedownload, processLevelMaxRetries);
|
||||
}
|
||||
|
||||
// since all of these operations can be asynch we will always do the actual work in the response handler
|
||||
// for the download
|
||||
void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, const QString& scriptOrURL, const QString& contents, bool isURL, bool success) {
|
||||
void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, const QString& scriptOrURL, const QString& contents, bool isURL, bool success , const QString& status) {
|
||||
if (QThread::currentThread() != thread()) {
|
||||
#ifdef THREAD_DEBUGGING
|
||||
qCDebug(scriptengine) << "*** WARNING *** ScriptEngine::entityScriptContentAvailable() called on wrong thread ["
|
||||
|
@ -1462,7 +1540,8 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co
|
|||
Q_ARG(const QString&, scriptOrURL),
|
||||
Q_ARG(const QString&, contents),
|
||||
Q_ARG(bool, isURL),
|
||||
Q_ARG(bool, success));
|
||||
Q_ARG(bool, success),
|
||||
Q_ARG(const QString&, status));
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -1478,22 +1557,19 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co
|
|||
newDetails.scriptText = scriptOrURL;
|
||||
|
||||
if (!success) {
|
||||
newDetails.status = ERROR_LOADING_SCRIPT;
|
||||
newDetails.errorInfo = "Failed to load script";
|
||||
_entityScripts[entityID] = newDetails;
|
||||
emit entityScriptDetailsUpdated();
|
||||
newDetails.status = EntityScriptStatus::ERROR_LOADING_SCRIPT;
|
||||
newDetails.errorInfo = "Failed to load script (" + status + ")";
|
||||
setEntityScriptDetails(entityID, newDetails);
|
||||
return;
|
||||
}
|
||||
|
||||
QScriptProgram program(contents, fileName);
|
||||
if (!hasCorrectSyntax(program, this)) {
|
||||
if (!isFileUrl) {
|
||||
scriptCache->addScriptToBadScriptList(scriptOrURL);
|
||||
}
|
||||
newDetails.status = ERROR_RUNNING_SCRIPT;
|
||||
newDetails.errorInfo = "Bad syntax";
|
||||
_entityScripts[entityID] = newDetails;
|
||||
emit entityScriptDetailsUpdated();
|
||||
auto syntaxError = lintScript(contents, fileName);
|
||||
QScriptProgram program { contents, fileName };
|
||||
if (!syntaxError.isNull() || program.isNull()) {
|
||||
newDetails.status = EntityScriptStatus::ERROR_RUNNING_SCRIPT;
|
||||
newDetails.errorInfo = QString("Bad syntax (%1)").arg(syntaxError);
|
||||
setEntityScriptDetails(entityID, newDetails);
|
||||
qCDebug(scriptengine) << newDetails.errorInfo << scriptOrURL;
|
||||
return; // done processing script
|
||||
}
|
||||
|
||||
|
@ -1502,16 +1578,18 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co
|
|||
}
|
||||
|
||||
const int SANDBOX_TIMEOUT = 0.25 * MSECS_PER_SECOND;
|
||||
QScriptEngine sandbox;
|
||||
BaseScriptEngine sandbox;
|
||||
sandbox.setProcessEventsInterval(SANDBOX_TIMEOUT);
|
||||
QScriptValue testConstructor;
|
||||
{
|
||||
QTimer timeout;
|
||||
timeout.setSingleShot(true);
|
||||
timeout.start(SANDBOX_TIMEOUT);
|
||||
connect(&timeout, &QTimer::timeout, [&sandbox, SANDBOX_TIMEOUT]{
|
||||
connect(&timeout, &QTimer::timeout, [&sandbox, SANDBOX_TIMEOUT, scriptOrURL]{
|
||||
auto context = sandbox.currentContext();
|
||||
if (context) {
|
||||
qCDebug(scriptengine) << "ScriptEngine::entityScriptContentAvailable timeout(" << scriptOrURL << ")";
|
||||
|
||||
// Guard against infinite loops and non-performant code
|
||||
context->throwError(QString("Timed out (entity constructors are limited to %1ms)").arg(SANDBOX_TIMEOUT));
|
||||
}
|
||||
|
@ -1519,13 +1597,12 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co
|
|||
testConstructor = sandbox.evaluate(program);
|
||||
}
|
||||
|
||||
QString exceptionMessage;
|
||||
if (hadUncaughtExceptions(sandbox, program.fileName(), this, &exceptionMessage)) {
|
||||
newDetails.status = ERROR_RUNNING_SCRIPT;
|
||||
QString exceptionMessage = sandbox.formatUncaughtException(program.fileName());
|
||||
if (!exceptionMessage.isNull()) {
|
||||
newDetails.status = EntityScriptStatus::ERROR_RUNNING_SCRIPT;
|
||||
newDetails.errorInfo = exceptionMessage;
|
||||
_entityScripts[entityID] = newDetails;
|
||||
emit entityScriptDetailsUpdated();
|
||||
|
||||
setEntityScriptDetails(entityID, newDetails);
|
||||
qCDebug(scriptengine) << "----- ScriptEngine::entityScriptContentAvailable -- hadUncaughtExceptions (" << scriptOrURL << ")";
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -1544,15 +1621,11 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co
|
|||
+ "," + testConstructorValue
|
||||
+ "," + scriptOrURL);
|
||||
|
||||
if (!isFileUrl) {
|
||||
scriptCache->addScriptToBadScriptList(scriptOrURL);
|
||||
}
|
||||
|
||||
newDetails.status = ERROR_RUNNING_SCRIPT;
|
||||
newDetails.status = EntityScriptStatus::ERROR_RUNNING_SCRIPT;
|
||||
newDetails.errorInfo = "Could not find constructor";
|
||||
_entityScripts[entityID] = newDetails;
|
||||
emit entityScriptDetailsUpdated();
|
||||
setEntityScriptDetails(entityID, newDetails);
|
||||
|
||||
qCDebug(scriptengine) << "----- ScriptEngine::entityScriptContentAvailable -- failed to run (" << scriptOrURL << ")";
|
||||
return; // done processing script
|
||||
}
|
||||
|
||||
|
@ -1569,11 +1642,11 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co
|
|||
};
|
||||
doWithEnvironment(entityID, sandboxURL, initialization);
|
||||
|
||||
newDetails.status = EntityScriptStatus::RUNNING;
|
||||
newDetails.scriptObject = entityScriptObject;
|
||||
newDetails.lastModified = lastModified;
|
||||
newDetails.definingSandboxURL = sandboxURL;
|
||||
_entityScripts[entityID] = newDetails;
|
||||
emit entityScriptDetailsUpdated();
|
||||
setEntityScriptDetails(entityID, newDetails);
|
||||
|
||||
if (isURL) {
|
||||
setParentURL("");
|
||||
|
@ -1600,12 +1673,13 @@ void ScriptEngine::unloadEntityScript(const EntityItemID& entityID) {
|
|||
#endif
|
||||
|
||||
if (_entityScripts.contains(entityID)) {
|
||||
if (_entityScripts[entityID].status == RUNNING) {
|
||||
if (isEntityScriptRunning(entityID)) {
|
||||
callEntityScriptMethod(entityID, "unload");
|
||||
}
|
||||
_entityScripts.remove(entityID);
|
||||
EntityScriptDetails newDetails;
|
||||
newDetails.status = EntityScriptStatus::UNLOADED;
|
||||
setEntityScriptDetails(entityID, newDetails);
|
||||
stopAllTimersForEntityScript(entityID);
|
||||
emit entityScriptDetailsUpdated();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1622,9 +1696,7 @@ void ScriptEngine::unloadAllEntityScripts() {
|
|||
qCDebug(scriptengine) << "ScriptEngine::unloadAllEntityScripts() called on correct thread [" << thread() << "]";
|
||||
#endif
|
||||
foreach(const EntityItemID& entityID, _entityScripts.keys()) {
|
||||
if (_entityScripts[entityID].status == RUNNING) {
|
||||
callEntityScriptMethod(entityID, "unload");
|
||||
}
|
||||
unloadEntityScript(entityID);
|
||||
}
|
||||
_entityScripts.clear();
|
||||
emit entityScriptDetailsUpdated();
|
||||
|
@ -1641,7 +1713,7 @@ void ScriptEngine::unloadAllEntityScripts() {
|
|||
}
|
||||
|
||||
void ScriptEngine::refreshFileScript(const EntityItemID& entityID) {
|
||||
if (!_entityScripts.contains(entityID)) {
|
||||
if (!HIFI_AUTOREFRESH_FILE_SCRIPTS || !_entityScripts.contains(entityID)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -1663,8 +1735,8 @@ void ScriptEngine::refreshFileScript(const EntityItemID& entityID) {
|
|||
file.open(QIODevice::ReadOnly);
|
||||
QString scriptContents = QTextStream(&file).readAll();
|
||||
this->unloadEntityScript(entityID);
|
||||
this->entityScriptContentAvailable(entityID, details.scriptText, scriptContents, true, true);
|
||||
if (!_entityScripts.contains(entityID) || _entityScripts[entityID].status != RUNNING) {
|
||||
this->entityScriptContentAvailable(entityID, details.scriptText, scriptContents, true, true, "Success");
|
||||
if (!isEntityScriptRunning(entityID)) {
|
||||
scriptWarningMessage("Reload script " + details.scriptText + " failed");
|
||||
} else {
|
||||
details = _entityScripts[entityID];
|
||||
|
@ -1692,7 +1764,10 @@ void ScriptEngine::doWithEnvironment(const EntityItemID& entityID, const QUrl& s
|
|||
#else
|
||||
operation();
|
||||
#endif
|
||||
hadUncaughtExceptions(*this, _fileNameString, this);
|
||||
if (hasUncaughtException()) {
|
||||
reportUncaughtException();
|
||||
clearExceptions();
|
||||
}
|
||||
|
||||
currentEntityIdentifier = oldIdentifier;
|
||||
currentSandboxURL = oldSandboxURL;
|
||||
|
@ -1722,8 +1797,10 @@ void ScriptEngine::callEntityScriptMethod(const EntityItemID& entityID, const QS
|
|||
"entityID:" << entityID << "methodName:" << methodName;
|
||||
#endif
|
||||
|
||||
refreshFileScript(entityID);
|
||||
if (_entityScripts.contains(entityID) && _entityScripts[entityID].status == RUNNING) {
|
||||
if (HIFI_AUTOREFRESH_FILE_SCRIPTS && methodName != "unload") {
|
||||
refreshFileScript(entityID);
|
||||
}
|
||||
if (isEntityScriptRunning(entityID)) {
|
||||
EntityScriptDetails details = _entityScripts[entityID];
|
||||
QScriptValue entityScript = details.scriptObject; // previously loaded
|
||||
if (entityScript.property(methodName).isFunction()) {
|
||||
|
@ -1754,8 +1831,10 @@ void ScriptEngine::callEntityScriptMethod(const EntityItemID& entityID, const QS
|
|||
"entityID:" << entityID << "methodName:" << methodName << "event: pointerEvent";
|
||||
#endif
|
||||
|
||||
refreshFileScript(entityID);
|
||||
if (_entityScripts.contains(entityID) && _entityScripts[entityID].status == RUNNING) {
|
||||
if (HIFI_AUTOREFRESH_FILE_SCRIPTS) {
|
||||
refreshFileScript(entityID);
|
||||
}
|
||||
if (isEntityScriptRunning(entityID)) {
|
||||
EntityScriptDetails details = _entityScripts[entityID];
|
||||
QScriptValue entityScript = details.scriptObject; // previously loaded
|
||||
if (entityScript.property(methodName).isFunction()) {
|
||||
|
@ -1787,8 +1866,10 @@ void ScriptEngine::callEntityScriptMethod(const EntityItemID& entityID, const QS
|
|||
"entityID:" << entityID << "methodName:" << methodName << "otherID:" << otherID << "collision: collision";
|
||||
#endif
|
||||
|
||||
refreshFileScript(entityID);
|
||||
if (_entityScripts.contains(entityID) && _entityScripts[entityID].status == RUNNING) {
|
||||
if (HIFI_AUTOREFRESH_FILE_SCRIPTS) {
|
||||
refreshFileScript(entityID);
|
||||
}
|
||||
if (isEntityScriptRunning(entityID)) {
|
||||
EntityScriptDetails details = _entityScripts[entityID];
|
||||
QScriptValue entityScript = details.scriptObject; // previously loaded
|
||||
if (entityScript.property(methodName).isFunction()) {
|
||||
|
|
|
@ -59,7 +59,7 @@ typedef QHash<QString, CallbackList> RegisteredEventHandlers;
|
|||
|
||||
class EntityScriptDetails {
|
||||
public:
|
||||
EntityScriptStatus status { RUNNING };
|
||||
EntityScriptStatus status { EntityScriptStatus::PENDING };
|
||||
|
||||
// If status indicates an error, this contains a human-readable string giving more information about the error.
|
||||
QString errorInfo { "" };
|
||||
|
@ -70,7 +70,15 @@ public:
|
|||
QUrl definingSandboxURL { QUrl() };
|
||||
};
|
||||
|
||||
class ScriptEngine : public QScriptEngine, public ScriptUser, public EntitiesScriptEngineProvider {
|
||||
// common base class with just QScriptEngine-dependent helper methods
|
||||
class BaseScriptEngine : public QScriptEngine {
|
||||
public:
|
||||
static const QString SCRIPT_EXCEPTION_FORMAT;
|
||||
QString lintScript(const QString& sourceCode, const QString& fileName, const int lineNumber = 1);
|
||||
QString formatUncaughtException(const QString& overrideFileName = QString());
|
||||
};
|
||||
|
||||
class ScriptEngine : public BaseScriptEngine, public EntitiesScriptEngineProvider {
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(QString context READ getContext)
|
||||
public:
|
||||
|
@ -82,6 +90,7 @@ public:
|
|||
AGENT_SCRIPT
|
||||
};
|
||||
|
||||
static int processLevelMaxRetries;
|
||||
ScriptEngine(Context context, const QString& scriptContents = NO_SCRIPT, const QString& fileNameString = QString(""));
|
||||
~ScriptEngine();
|
||||
|
||||
|
@ -89,6 +98,7 @@ public:
|
|||
/// the current script contents and calling run(). Callers will likely want to register the script with external
|
||||
/// services before calling this.
|
||||
void runInThread();
|
||||
Q_INVOKABLE void executeOnScriptThread(std::function<void()> function, bool blocking = false);
|
||||
|
||||
void runDebuggable();
|
||||
|
||||
|
@ -157,6 +167,10 @@ public:
|
|||
Q_INVOKABLE QUrl resourcesPath() const;
|
||||
|
||||
// Entity Script Related methods
|
||||
Q_INVOKABLE QString getEntityScriptStatus(const EntityItemID& entityID);
|
||||
Q_INVOKABLE bool isEntityScriptRunning(const EntityItemID& entityID) {
|
||||
return _entityScripts.contains(entityID) && _entityScripts[entityID].status == EntityScriptStatus::RUNNING;
|
||||
}
|
||||
static void loadEntityScript(QWeakPointer<ScriptEngine> theEngine, const EntityItemID& entityID, const QString& entityScript, bool forceRedownload);
|
||||
Q_INVOKABLE void unloadEntityScript(const EntityItemID& entityID); // will call unload method
|
||||
Q_INVOKABLE void unloadAllEntityScripts();
|
||||
|
@ -180,11 +194,6 @@ public:
|
|||
void disconnectNonEssentialSignals();
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// NOTE - These are the callback implementations for ScriptUser the get called by ScriptCache when the contents
|
||||
// of a script are available.
|
||||
virtual void scriptContentsAvailable(const QUrl& url, const QString& scriptContents) override;
|
||||
virtual void errorInLoadingScript(const QUrl& url) override;
|
||||
|
||||
// These are currently used by Application to track if a script is user loaded or not. Consider finding a solution
|
||||
// inside of Application so that the ScriptEngine class is not polluted by this notion
|
||||
void setUserLoaded(bool isUserLoaded) { _isUserLoaded = isUserLoaded; }
|
||||
|
@ -203,6 +212,7 @@ public:
|
|||
bool getEntityScriptDetails(const EntityItemID& entityID, EntityScriptDetails &details) const;
|
||||
|
||||
public slots:
|
||||
int evaluatePending() const { return _evaluatesPending; }
|
||||
void callAnimationStateHandler(QScriptValue callback, AnimVariantMap parameters, QStringList names, bool useNames, AnimVariantResultHandler resultHandler);
|
||||
void updateMemoryCost(const qint64&);
|
||||
|
||||
|
@ -230,12 +240,13 @@ signals:
|
|||
protected:
|
||||
void init();
|
||||
|
||||
bool evaluatePending() const { return _evaluatesPending > 0; }
|
||||
QString reportUncaughtException(const QString& overrideFileName = QString());
|
||||
void timerFired();
|
||||
void stopAllTimers();
|
||||
void stopAllTimersForEntityScript(const EntityItemID& entityID);
|
||||
void refreshFileScript(const EntityItemID& entityID);
|
||||
|
||||
void updateEntityScriptStatus(const EntityItemID& entityID, const EntityScriptStatus& status, const QString& errorInfo = QString());
|
||||
void setEntityScriptDetails(const EntityItemID& entityID, const EntityScriptDetails& details);
|
||||
void setParentURL(const QString& parentURL) { _parentURL = parentURL; }
|
||||
|
||||
QObject* setupTimerWithInterval(const QScriptValue& function, int intervalMS, bool isSingleShot);
|
||||
|
@ -243,7 +254,7 @@ protected:
|
|||
|
||||
QHash<EntityItemID, RegisteredEventHandlers> _registeredHandlers;
|
||||
void forwardHandlerCall(const EntityItemID& entityID, const QString& eventName, QScriptValueList eventHanderArgs);
|
||||
Q_INVOKABLE void entityScriptContentAvailable(const EntityItemID& entityID, const QString& scriptOrURL, const QString& contents, bool isURL, bool success);
|
||||
Q_INVOKABLE void entityScriptContentAvailable(const EntityItemID& entityID, const QString& scriptOrURL, const QString& contents, bool isURL, bool success, const QString& status);
|
||||
|
||||
EntityItemID currentEntityIdentifier {}; // Contains the defining entity script entity id during execution, if any. Empty for interface script execution.
|
||||
QUrl currentSandboxURL {}; // The toplevel url string for the entity script that loaded the code being executed, else empty.
|
||||
|
|
|
@ -27,6 +27,8 @@
|
|||
|
||||
static const QString DESKTOP_LOCATION = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation);
|
||||
|
||||
static const bool HIFI_SCRIPT_DEBUGGABLES { true };
|
||||
|
||||
ScriptsModel& getScriptsModel() {
|
||||
static ScriptsModel scriptsModel;
|
||||
return scriptsModel;
|
||||
|
@ -517,8 +519,9 @@ void ScriptEngines::launchScriptEngine(ScriptEngine* scriptEngine) {
|
|||
for (auto initializer : _scriptInitializers) {
|
||||
initializer(scriptEngine);
|
||||
}
|
||||
|
||||
if (scriptEngine->isDebuggable() || (qApp->queryKeyboardModifiers() & Qt::ShiftModifier)) {
|
||||
|
||||
auto const wantDebug = scriptEngine->isDebuggable() || (qApp->queryKeyboardModifiers() & Qt::ShiftModifier);
|
||||
if (HIFI_SCRIPT_DEBUGGABLES && wantDebug) {
|
||||
scriptEngine->runDebuggable();
|
||||
} else {
|
||||
scriptEngine->runInThread();
|
||||
|
|
|
@ -1470,8 +1470,8 @@ var PropertiesTool = function (opts) {
|
|||
|
||||
function resetScriptStatus() {
|
||||
updateScriptStatus({
|
||||
statusRetrieved: false,
|
||||
isRunning: false,
|
||||
statusRetrieved: undefined,
|
||||
isRunning: undefined,
|
||||
status: "",
|
||||
errorInfo: ""
|
||||
});
|
||||
|
|
|
@ -318,6 +318,7 @@
|
|||
<input type="text" id="property-script-url">
|
||||
<input type="button" id="reload-script-button" class="glyph" value="F">
|
||||
</div>
|
||||
<hr class="behavior-group" />
|
||||
<div class="behavior-group property url refresh">
|
||||
<label for="property-server-scripts">Server Script URL</label>
|
||||
<input type="text" id="property-server-scripts">
|
||||
|
|
|
@ -713,24 +713,22 @@ function loaded() {
|
|||
EventBridge.scriptEventReceived.connect(function(data) {
|
||||
data = JSON.parse(data);
|
||||
if (data.type == "server_script_status") {
|
||||
if (!data.statusRetrieved) {
|
||||
elServerScriptStatus.innerHTML = "Failed to retrieve status";
|
||||
elServerScriptError.style.display = "none";
|
||||
elServerScriptError.value = data.errorInfo;
|
||||
elServerScriptError.style.display = data.errorInfo ? "block" : "none";
|
||||
if (data.statusRetrieved === false) {
|
||||
elServerScriptStatus.innerText = "Failed to retrieve status";
|
||||
} else if (data.isRunning) {
|
||||
if (data.status == "running") {
|
||||
elServerScriptStatus.innerHTML = "Running";
|
||||
elServerScriptError.style.display = "none";
|
||||
} else if (data.status == "error_loading_script") {
|
||||
elServerScriptStatus.innerHTML = "Error loading script";
|
||||
elServerScriptError.style.display = "block";
|
||||
} else if (data.status == "error_running_script") {
|
||||
elServerScriptStatus.innerHTML = "Error running script";
|
||||
elServerScriptError.style.display = "block";
|
||||
}
|
||||
elServerScriptError.innerHTML = data.errorInfo;;
|
||||
var ENTITY_SCRIPT_STATUS = {
|
||||
pending: "Pending",
|
||||
loading: "Loading",
|
||||
error_loading_script: "Error loading script",
|
||||
error_running_script: "Error running script",
|
||||
running: "Running",
|
||||
unloaded: "Unloaded",
|
||||
};
|
||||
elServerScriptStatus.innerText = ENTITY_SCRIPT_STATUS[data.status] || data.status;
|
||||
} else {
|
||||
elServerScriptStatus.innerHTML = "Not running";
|
||||
elServerScriptError.style.display = "none";
|
||||
elServerScriptStatus.innerText = "Not running";
|
||||
}
|
||||
} else if (data.type == "update") {
|
||||
|
||||
|
@ -1169,6 +1167,10 @@ function loaded() {
|
|||
elScriptURL.addEventListener('change', createEmitTextPropertyUpdateFunction('script'));
|
||||
elScriptTimestamp.addEventListener('change', createEmitNumberPropertyUpdateFunction('scriptTimestamp'));
|
||||
elServerScripts.addEventListener('change', createEmitTextPropertyUpdateFunction('serverScripts'));
|
||||
elServerScripts.addEventListener('change', function() {
|
||||
// invalidate the current status (so that same-same updates can still be observed visually)
|
||||
elServerScriptStatus.innerText = '[' + elServerScriptStatus.innerText + ']';
|
||||
});
|
||||
|
||||
elClearUserData.addEventListener("click", function() {
|
||||
deleteJSONEditor();
|
||||
|
@ -1428,6 +1430,8 @@ function loaded() {
|
|||
}));
|
||||
});
|
||||
elReloadServerScriptsButton.addEventListener("click", function() {
|
||||
// invalidate the current status (so that same-same updates can still be observed visually)
|
||||
elServerScriptStatus.innerText = '[' + elServerScriptStatus.innerText + ']';
|
||||
EventBridge.emitWebEvent(JSON.stringify({
|
||||
type: "action",
|
||||
action: "reloadServerScripts"
|
||||
|
|
Loading…
Reference in a new issue