Merge pull request #9656 from humbletim/21114-part1

CR-1 21114 -- ScriptCache/ScriptEngine cleanup, loader states, accessible load errors
This commit is contained in:
Seth Alves 2017-02-14 06:49:05 -08:00 committed by GitHub
commit 4393bc3c2e
14 changed files with 319 additions and 268 deletions

View file

@ -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();
});

View file

@ -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();
}

View file

@ -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

View file

@ -38,6 +38,7 @@ public:
InvalidURL,
NotFound
};
Q_ENUM(Result)
QByteArray getData() { return _data; }
State getState() const { return _state; }

View file

@ -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);
}

View file

@ -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

View file

@ -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);
}
}
}

View file

@ -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

View file

@ -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()) {

View file

@ -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.

View file

@ -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();

View file

@ -1470,8 +1470,8 @@ var PropertiesTool = function (opts) {
function resetScriptStatus() {
updateScriptStatus({
statusRetrieved: false,
isRunning: false,
statusRetrieved: undefined,
isRunning: undefined,
status: "",
errorInfo: ""
});

View file

@ -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">

View file

@ -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"