Merge pull request #16 from huffman/feat/entity-server-script-property

Add server script status to edit.js
This commit is contained in:
Clément Brisset 2017-01-19 15:34:29 -08:00 committed by GitHub
commit c2ca76f630
10 changed files with 279 additions and 75 deletions

View file

@ -11,6 +11,8 @@
#include "EntityScriptServer.h"
#include "EntityScriptUtils.h"
#include <AudioConstants.h>
#include <AudioInjectorManager.h>
#include <EntityScriptingInterface.h>
@ -21,12 +23,11 @@
#include <ScriptCache.h>
#include <ScriptEngines.h>
#include <SoundCache.h>
#include <UUID.h>
#include <WebSocketServerClass.h>
#include "../entities/AssignmentParentFinder.h"
const size_t UUID_LENGTH_BYTES = 16;
int EntityScriptServer::_entitiesScriptEngineCount = 0;
EntityScriptServer::EntityScriptServer(ReceivedMessage& message) : ThreadedAssignment(message) {
@ -62,29 +63,42 @@ EntityScriptServer::EntityScriptServer(ReceivedMessage& message) : ThreadedAssig
static const QString ENTITY_SCRIPT_SERVER_LOGGING_NAME = "entity-script-server";
void EntityScriptServer::handleReloadEntityServerScriptPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode) {
auto entityID = QUuid::fromRfc4122(message->read(UUID_LENGTH_BYTES));
// These are temporary checks until we can ensure that nodes eventually disconnect if the Domain Server stops telling them
// about each other.
if (senderNode->getCanRez() || senderNode->getCanRezTmp()) {
auto entityID = QUuid::fromRfc4122(message->read(NUM_BYTES_RFC4122_UUID));
if (_entityViewer.getTree() && !_shuttingDown) {
qDebug() << "Reloading: " << entityID;
_entitiesScriptEngine->unloadEntityScript(entityID);
checkAndCallPreload(entityID, true);
if (_entityViewer.getTree() && !_shuttingDown) {
qDebug() << "Reloading: " << entityID;
_entitiesScriptEngine->unloadEntityScript(entityID);
checkAndCallPreload(entityID, true);
}
}
}
void EntityScriptServer::handleEntityScriptGetStatusPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode) {
MessageID messageID;
message->readPrimitive(&messageID);
auto entityID = QUuid::fromRfc4122(message->read(UUID_LENGTH_BYTES));
// These are temporary checks until we can ensure that nodes eventually disconnect if the Domain Server stops telling them
// about each other.
if (senderNode->getCanRez() || senderNode->getCanRezTmp()) {
MessageID messageID;
message->readPrimitive(&messageID);
auto entityID = QUuid::fromRfc4122(message->read(NUM_BYTES_RFC4122_UUID));
// TODO(Huffman) Get Status
qDebug() << "Getting script status of: " << entityID;
auto replyPacket = NLPacket::create(PacketType::EntityScriptGetStatusReply, -1, true);
replyPacket->writePrimitive(messageID);
replyPacket->writeString("running");
auto replyPacketList = NLPacketList::create(PacketType::EntityScriptGetStatusReply, QByteArray(), true, true);
replyPacketList->writePrimitive(messageID);
auto nodeList = DependencyManager::get<NodeList>();
nodeList->sendPacket(std::move(replyPacket), *senderNode);
EntityScriptDetails details;
if (_entitiesScriptEngine->getEntityScriptDetails(entityID, details)) {
replyPacketList->writePrimitive(true);
replyPacketList->writePrimitive(details.status);
replyPacketList->writeString(details.errorInfo);
} else {
replyPacketList->writePrimitive(false);
}
auto nodeList = DependencyManager::get<NodeList>();
nodeList->sendPacketList(std::move(replyPacketList), *senderNode);
}
}
void EntityScriptServer::run() {

View file

@ -953,21 +953,32 @@ void DomainServer::sendDomainListToNode(const SharedNodePointer& node, const Hif
// DTLSServerSession* dtlsSession = _isUsingDTLS ? _dtlsSessions[senderSockAddr] : NULL;
if (nodeData->isAuthenticated()) {
// if this authenticated node has any interest types, send back those nodes as well
limitedNodeList->eachNode([&](const SharedNodePointer& otherNode){
limitedNodeList->eachNode([&](const SharedNodePointer& otherNode) {
if (otherNode->getUUID() != node->getUUID()
&& nodeInterestSet.contains(otherNode->getType())) {
// since we're about to add a node to the packet we start a segment
domainListPackets->startSegment();
// don't send avatar nodes to other avatars, that will come from avatar mixer
domainListStream << *otherNode.data();
// (1/19/17) Agents only need to connect to Entity Script Servers to perform administrative tasks
// related to entity server scripts. Only agents with rez permissions should be doing that, so
// if the agent does not have those permissions, we do not want them and the server to incur the
// overhead of connecting to one another.
bool shouldNotConnect = (node->getType() == NodeType::Agent && otherNode->getType() == NodeType::EntityScriptServer
&& !node->getCanRez() && !node->getCanRezTmp())
|| (node->getType() == NodeType::EntityScriptServer && otherNode->getType() == NodeType::Agent
&& !otherNode->getCanRez() && !otherNode->getCanRezTmp());
// pack the secret that these two nodes will use to communicate with each other
domainListStream << connectionSecretForNodes(node, otherNode);
if (!shouldNotConnect) {
// since we're about to add a node to the packet we start a segment
domainListPackets->startSegment();
// we've added the node we wanted so end the segment now
domainListPackets->endSegment();
// don't send avatar nodes to other avatars, that will come from avatar mixer
domainListStream << *otherNode.data();
// pack the secret that these two nodes will use to communicate with each other
domainListStream << connectionSecretForNodes(node, otherNode);
// we've added the node we wanted so end the segment now
domainListPackets->endSegment();
}
}
});
}

View file

@ -680,7 +680,22 @@ 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 {
QScriptValueList args { true };
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() };
callback.call(QScriptValue(), args);
request->deleteLater();
});

View file

@ -1,6 +1,7 @@
#include "EntityScriptClient.h"
#include "NodeList.h"
#include "NetworkLogging.h"
#include "EntityScriptUtils.h"
#include <QThread>
@ -15,9 +16,12 @@ GetScriptStatusRequest::~GetScriptStatusRequest() {
void GetScriptStatusRequest::start() {
auto client = DependencyManager::get<EntityScriptClient>();
qDebug() << "Sending script status request";
client->getEntityServerScriptStatus(_entityID, [this](bool responseReceived) {
qDebug() << "Received script status request";
client->getEntityServerScriptStatus(_entityID, [this](bool responseReceived, bool isRunning, EntityScriptStatus status, QString errorInfo) {
_responseReceived = responseReceived;
_isRunning = isRunning;
_status = status;
_errorInfo = errorInfo;
emit finished(this);
});
}
@ -84,7 +88,7 @@ MessageID EntityScriptClient::getEntityServerScriptStatus(QUuid entityID, GetScr
}
}
callback(false);
callback(false, false, ERROR_LOADING_SCRIPT, "");
return INVALID_MESSAGE_ID;
}
@ -92,9 +96,17 @@ void EntityScriptClient::handleGetScriptStatusReply(QSharedPointer<ReceivedMessa
Q_ASSERT(QThread::currentThread() == thread());
MessageID messageID;
message->readPrimitive(&messageID);
bool isKnown { false };
EntityScriptStatus status = ERROR_LOADING_SCRIPT;
QString errorInfo { "" };
auto status = message->readString();
message->readPrimitive(&messageID);
message->readPrimitive(&isKnown);
if (isKnown) {
message->readPrimitive(&status);
errorInfo = message->readString();
}
// Check if we have any pending requests for this node
auto messageMapIt = _pendingEntityScriptStatusRequests.find(senderNode);
@ -107,7 +119,7 @@ void EntityScriptClient::handleGetScriptStatusReply(QSharedPointer<ReceivedMessa
auto requestIt = messageCallbackMap.find(messageID);
if (requestIt != messageCallbackMap.end()) {
auto callback = requestIt->second;
callback(true);
callback(true, isKnown, status, errorInfo);
messageCallbackMap.erase(requestIt);
}
@ -145,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);
value.second(false, false, ERROR_LOADING_SCRIPT, "");
}
messageMapIt->second.clear();
}

View file

@ -15,14 +15,14 @@
#include "LimitedNodeList.h"
#include "ReceivedMessage.h"
#include "AssetUtils.h"
#include "EntityScriptUtils.h"
#include <DependencyManager.h>
#include <cstdint>
#include <unordered_map>
using MessageID = uint32_t;
using GetScriptStatusCallback = std::function<void(bool responseReceived)>;
using GetScriptStatusCallback = std::function<void(bool responseReceived, bool isRunning, EntityScriptStatus status, QString errorInfo)>;
class GetScriptStatusRequest : public QObject {
Q_OBJECT
@ -32,13 +32,22 @@ public:
Q_INVOKABLE void start();
bool getResponseReceived() const { return _responseReceived; }
bool getIsRunning() const { return _isRunning; }
EntityScriptStatus getStatus() const { return _status; }
QString getErrorInfo() const { return _errorInfo; }
signals:
void finished(GetScriptStatusRequest* request);
private:
QUuid _entityID;
MessageID _messageID;
QString status;
bool _responseReceived;
bool _isRunning;
EntityScriptStatus _status;
QString _errorInfo;
};
class EntityScriptClient : public QObject, public Dependency {

View file

@ -1374,6 +1374,15 @@ void ScriptEngine::forwardHandlerCall(const EntityItemID& entityID, const QStrin
}
}
bool ScriptEngine::getEntityScriptDetails(const EntityItemID& entityID, EntityScriptDetails &details) const {
auto it = _entityScripts.constFind(entityID);
if (it == _entityScripts.constEnd()) {
return false;
}
details = it.value();
return true;
}
// 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) {
@ -1421,11 +1430,24 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co
bool isFileUrl = isURL && scriptOrURL.startsWith("file://");
auto fileName = isURL ? scriptOrURL : "EmbeddedEntityScript";
EntityScriptDetails newDetails;
newDetails.scriptText = scriptOrURL;
if (!success) {
newDetails.status = ERROR_LOADING_SCRIPT;
newDetails.errorInfo = "Failed to load script";
_entityScripts[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;
return; // done processing script
}
@ -1451,6 +1473,10 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co
testConstructor = sandbox.evaluate(program);
}
if (hadUncaughtExceptions(sandbox, program.fileName(), this)) {
newDetails.status = ERROR_RUNNING_SCRIPT;
newDetails.errorInfo = "Exception";
_entityScripts[entityID] = newDetails;
return;
}
@ -1473,6 +1499,10 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co
scriptCache->addScriptToBadScriptList(scriptOrURL);
}
newDetails.status = ERROR_RUNNING_SCRIPT;
newDetails.errorInfo = "Could not find constructor";
_entityScripts[entityID] = newDetails;
return; // done processing script
}
@ -1489,8 +1519,11 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co
};
doWithEnvironment(entityID, sandboxURL, initialization);
EntityScriptDetails newDetails = { scriptOrURL, entityScriptObject, lastModified, sandboxURL };
newDetails.scriptObject = entityScriptObject;
newDetails.lastModified = lastModified;
newDetails.definingSandboxURL = sandboxURL;
_entityScripts[entityID] = newDetails;
if (isURL) {
setParentURL("");
}
@ -1516,7 +1549,9 @@ void ScriptEngine::unloadEntityScript(const EntityItemID& entityID) {
#endif
if (_entityScripts.contains(entityID)) {
callEntityScriptMethod(entityID, "unload");
if (_entityScripts[entityID].status == RUNNING) {
callEntityScriptMethod(entityID, "unload");
}
_entityScripts.remove(entityID);
stopAllTimersForEntityScript(entityID);
}
@ -1535,7 +1570,9 @@ void ScriptEngine::unloadAllEntityScripts() {
qCDebug(scriptengine) << "ScriptEngine::unloadAllEntityScripts() called on correct thread [" << thread() << "]";
#endif
foreach(const EntityItemID& entityID, _entityScripts.keys()) {
callEntityScriptMethod(entityID, "unload");
if (_entityScripts[entityID].status == RUNNING) {
callEntityScriptMethod(entityID, "unload");
}
}
_entityScripts.clear();
@ -1574,7 +1611,7 @@ void ScriptEngine::refreshFileScript(const EntityItemID& entityID) {
QString scriptContents = QTextStream(&file).readAll();
this->unloadEntityScript(entityID);
this->entityScriptContentAvailable(entityID, details.scriptText, scriptContents, true, true);
if (!_entityScripts.contains(entityID)) {
if (!_entityScripts.contains(entityID) || _entityScripts[entityID].status != RUNNING) {
scriptWarningMessage("Reload script " + details.scriptText + " failed");
} else {
details = _entityScripts[entityID];
@ -1633,7 +1670,7 @@ void ScriptEngine::callEntityScriptMethod(const EntityItemID& entityID, const QS
#endif
refreshFileScript(entityID);
if (_entityScripts.contains(entityID)) {
if (_entityScripts.contains(entityID) && _entityScripts[entityID].status == RUNNING) {
EntityScriptDetails details = _entityScripts[entityID];
QScriptValue entityScript = details.scriptObject; // previously loaded
if (entityScript.property(methodName).isFunction()) {
@ -1665,7 +1702,7 @@ void ScriptEngine::callEntityScriptMethod(const EntityItemID& entityID, const QS
#endif
refreshFileScript(entityID);
if (_entityScripts.contains(entityID)) {
if (_entityScripts.contains(entityID) && _entityScripts[entityID].status == RUNNING) {
EntityScriptDetails details = _entityScripts[entityID];
QScriptValue entityScript = details.scriptObject; // previously loaded
if (entityScript.property(methodName).isFunction()) {
@ -1698,7 +1735,7 @@ void ScriptEngine::callEntityScriptMethod(const EntityItemID& entityID, const QS
#endif
refreshFileScript(entityID);
if (_entityScripts.contains(entityID)) {
if (_entityScripts.contains(entityID) && _entityScripts[entityID].status == RUNNING) {
EntityScriptDetails details = _entityScripts[entityID];
QScriptValue entityScript = details.scriptObject; // previously loaded
if (entityScript.property(methodName).isFunction()) {

View file

@ -29,6 +29,7 @@
#include <LimitedNodeList.h>
#include <EntityItemID.h>
#include <EntitiesScriptEngineProvider.h>
#include <EntityScriptUtils.h>
#include "PointerEvent.h"
#include "ArrayBufferClass.h"
@ -58,10 +59,15 @@ typedef QHash<QString, CallbackList> RegisteredEventHandlers;
class EntityScriptDetails {
public:
QString scriptText;
QScriptValue scriptObject;
int64_t lastModified;
QUrl definingSandboxURL;
EntityScriptStatus status { RUNNING };
// If status indicates an error, this contains a human-readable string giving more information about the error.
QString errorInfo { "" };
QString scriptText { "" };
QScriptValue scriptObject { QScriptValue() };
int64_t lastModified { 0 };
QUrl definingSandboxURL { QUrl() };
};
class ScriptEngine : public QScriptEngine, public ScriptUser, public EntitiesScriptEngineProvider {
@ -178,6 +184,8 @@ public:
void scriptWarningMessage(const QString& message);
void scriptInfoMessage(const QString& message);
bool getEntityScriptDetails(const EntityItemID& entityID, EntityScriptDetails &details) const;
public slots:
void callAnimationStateHandler(QScriptValue callback, AnimVariantMap parameters, QStringList names, bool useNames, AnimVariantResultHandler resultHandler);
void updateMemoryCost(const qint64&);
@ -200,6 +208,28 @@ signals:
void doneRunning();
protected:
void init();
bool evaluatePending() const { return _evaluatesPending > 0; }
void timerFired();
void stopAllTimers();
void stopAllTimersForEntityScript(const EntityItemID& entityID);
void refreshFileScript(const EntityItemID& entityID);
void setParentURL(const QString& parentURL) { _parentURL = parentURL; }
QObject* setupTimerWithInterval(const QScriptValue& function, int intervalMS, bool isSingleShot);
void stopTimer(QTimer* timer);
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);
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.
void doWithEnvironment(const EntityItemID& entityID, const QUrl& sandboxURL, std::function<void()> operation);
void callWithEnvironment(const EntityItemID& entityID, const QUrl& sandboxURL, QScriptValue function, QScriptValue thisObject, QScriptValueList args);
QString _scriptContents;
QString _parentURL;
std::atomic<bool> _isFinished { false };
@ -215,19 +245,6 @@ protected:
bool _debuggable { false };
qint64 _lastUpdate;
void init();
bool evaluatePending() const { return _evaluatesPending > 0; }
void timerFired();
void stopAllTimers();
void stopAllTimersForEntityScript(const EntityItemID& entityID);
void refreshFileScript(const EntityItemID& entityID);
void setParentURL(const QString& parentURL) { _parentURL = parentURL; }
QObject* setupTimerWithInterval(const QScriptValue& function, int intervalMS, bool isSingleShot);
void stopTimer(QTimer* timer);
QString _fileNameString;
Quat _quatLibrary;
Vec3 _vec3Library;
@ -240,15 +257,6 @@ protected:
AssetScriptingInterface _assetScriptingInterface{ this };
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);
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.
void doWithEnvironment(const EntityItemID& entityID, const QUrl& sandboxURL, std::function<void()> operation);
void callWithEnvironment(const EntityItemID& entityID, const QUrl& sandboxURL, QScriptValue function, QScriptValue thisObject, QScriptValueList args);
std::function<bool()> _emitScriptUpdates{ [](){ return true; } };
std::recursive_mutex _lock;

View file

@ -1388,6 +1388,37 @@ function pushCommandForSelections(createdEntityData, deletedEntityData) {
var ENTITY_PROPERTIES_URL = Script.resolvePath('html/entityProperties.html');
var ServerScriptStatusMonitor = function(entityID, statusCallback) {
var self = this;
self.entityID = entityID;
self.active = true;
self.sendRequestTimerID = null;
var onStatusReceived = function(success, isRunning, status, errorInfo) {
print("Got script status:", success, isRunning, status, errorInfo);
if (self.active) {
print("Requesting status of script");
statusCallback({
statusRetrieved: success,
isRunning: isRunning,
status: status,
errorInfo: errorInfo
});
self.sendRequestTimerID = Script.setTimeout(function() {
if (self.active) {
Entities.getServerScriptStatus(entityID, onStatusReceived);
}
}, 1000);
};
};
self.stop = function() {
self.active = false;
}
Entities.getServerScriptStatus(entityID, onStatusReceived);
};
var PropertiesTool = function (opts) {
var that = {};
@ -1399,6 +1430,11 @@ var PropertiesTool = function (opts) {
var visible = false;
// This keeps track of the last entity ID that was selected. If multiple entities
// are selected or if no entity is selected this will be `null`.
var currentSelectedEntityID = null;
var statusMonitor = null;
webView.setVisible(visible);
that.setVisible = function (newVisible) {
@ -1406,10 +1442,43 @@ var PropertiesTool = function (opts) {
webView.setVisible(visible);
};
function updateScriptStatus(info) {
print("Got status: ", info);
info.type = "server_script_status";
webView.emitScriptEvent(JSON.stringify(info));
};
function resetScriptStatus() {
updateScriptStatus({
statusRetrieved: false,
isRunning: false,
status: "",
errorInfo: ""
});
}
selectionManager.addEventListener(function () {
var data = {
type: 'update'
};
resetScriptStatus();
if (selectionManager.selections.length !== 1) {
if (statusMonitor !== null) {
statusMonitor.stop();
statusMonitor = null;
}
currentSelectedEntityID = null;
} else if (currentSelectedEntityID != selectionManager.selections[0]) {
if (statusMonitor !== null) {
statusMonitor.stop();
}
var entityID = selectionManager.selections[0];
currentSelectedEntityID = entityID;
statusMonitor = new ServerScriptStatusMonitor(entityID, updateScriptStatus);
}
var selections = [];
for (var i = 0; i < selectionManager.selections.length; i++) {
var entity = {};

View file

@ -319,10 +319,17 @@
<input type="button" id="reload-script-button" class="glyph" value="F">
</div>
<div class="behavior-group property url refresh">
<label for="property-server-scripts">Server Script URLs</label>
<label for="property-server-scripts">Server Script URL</label>
<input type="text" id="property-server-scripts">
<input type="button" id="reload-server-scripts-button" class="glyph" value="F">
</div>
<div class="behavior-group property">
<label for="server-script-status">Server Script Status</label>
<span id="server-script-status"></span>
</div>
<div class="behavior-group property">
<textarea id="server-script-error"></textarea>
</div>
<div class="section-header model-group model-section zone-section">
<label>Model</label><span>M</span>
</div>

View file

@ -593,6 +593,8 @@ function loaded() {
var elReloadScriptsButton = document.getElementById("reload-script-button");
var elServerScripts = document.getElementById("property-server-scripts");
var elReloadServerScriptsButton = document.getElementById("reload-server-scripts-button");
var elServerScriptStatus = document.getElementById("server-script-status");
var elServerScriptError = document.getElementById("server-script-error");
var elUserData = document.getElementById("property-user-data");
var elClearUserData = document.getElementById("userdata-clear");
var elSaveUserData = document.getElementById("userdata-save");
@ -710,7 +712,27 @@ function loaded() {
var properties;
EventBridge.scriptEventReceived.connect(function(data) {
data = JSON.parse(data);
if (data.type == "update") {
if (data.type == "server_script_status") {
if (!data.statusRetrieved) {
elServerScriptStatus.innerHTML = "Failed to retrieve status";
elServerScriptError.style.display = "none";
} 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;;
} else {
elServerScriptStatus.innerHTML = "Not running";
elServerScriptError.style.display = "none";
}
} else if (data.type == "update") {
if (data.selections.length == 0) {
if (editor !== null && lastEntityID !== null) {