add entities file replacement to DS and ES

This commit is contained in:
Stephen Birarda 2017-04-27 16:51:24 -07:00
parent 0435749f82
commit a21a34a4a4
13 changed files with 248 additions and 16 deletions

View file

@ -11,12 +11,14 @@
#include "OctreeServer.h"
#include <QJsonDocument>
#include <QJsonObject>
#include <QTimer>
#include <time.h>
#include <AccountManager.h>
#include <Gzip.h>
#include <HTTPConnection.h>
#include <LogHandler.h>
#include <shared/NetworkUtils.h>
@ -924,6 +926,57 @@ void OctreeServer::handleJurisdictionRequestPacket(QSharedPointer<ReceivedMessag
_jurisdictionSender->queueReceivedPacket(message, senderNode);
}
void OctreeServer::handleOctreeFileReplacement(QSharedPointer<ReceivedMessage> message) {
if (!_isFinished && !_isShuttingDown) {
// these messages are only allowed to come from the domain server, so make sure that is the case
auto nodeList = DependencyManager::get<NodeList>();
if (message->getSenderSockAddr() == nodeList->getDomainHandler().getSockAddr()) {
// it's far cleaner to load up the new content upon server startup
// so here we just store a special file at our persist path
// and then force a stop of the server so that it can pick it up when it relaunches
if (!_persistAbsoluteFilePath.isEmpty()) {
// before we restart the server and make it try and load this data, let's make sure it is valid
auto compressedOctree = message->getMessage();
QByteArray jsonOctree;
// assume we have GZipped content
bool wasCompressed = gunzip(compressedOctree, jsonOctree);
if (!wasCompressed) {
// the source was not compressed, assume we were sent regular JSON data
jsonOctree = compressedOctree;
}
// check the JSON data to verify it is an object
if (QJsonDocument::fromJson(jsonOctree).isObject()) {
if (!wasCompressed) {
// source was not compressed, we compress it before we write it locally
gzip(jsonOctree, compressedOctree);
}
// write the compressed octree data to a special file
auto replacementFilePath = _persistAbsoluteFilePath.append(OctreePersistThread::REPLACEMENT_FILE_EXTENSION);
QFile replacementFile(replacementFilePath);
if (replacementFile.open(QIODevice::WriteOnly) && replacementFile.write(compressedOctree) != -1) {
// we've now written our replacement file, time to take the server down so it can
// process it when it comes back up
qInfo() << "Wrote octree replacement file to" << replacementFilePath << "- stopping server";
setFinished(true);
} else {
qWarning() << "Could not write replacement octree data to file - refusing to process";
}
} else {
qDebug() << "Received replacement octree file that is invalid - refusing to process";
}
} else {
qDebug() << "Cannot perform octree file replacement since current persist file path is not yet known";
}
} else {
qDebug() << "Received an octree file replacement that was not from our domain server - refusing to process";
}
}
}
bool OctreeServer::readOptionBool(const QString& optionName, const QJsonObject& settingsSectionObject, bool& result) {
result = false; // assume it doesn't exist
bool optionAvailable = false;
@ -1148,6 +1201,7 @@ void OctreeServer::domainSettingsRequestComplete() {
packetReceiver.registerListener(getMyQueryMessageType(), this, "handleOctreeQueryPacket");
packetReceiver.registerListener(PacketType::OctreeDataNack, this, "handleOctreeDataNackPacket");
packetReceiver.registerListener(PacketType::JurisdictionRequest, this, "handleJurisdictionRequestPacket");
packetReceiver.registerListener(PacketType::OctreeFileReplacement, this, "handleOctreeFileReplacement");
readConfiguration();
@ -1173,25 +1227,25 @@ void OctreeServer::domainSettingsRequestComplete() {
// If persist filename does not exist, let's see if there is one beside the application binary
// If there is, let's copy it over to our target persist directory
QDir persistPath { _persistFilePath };
QString persistAbsoluteFilePath = persistPath.absolutePath();
_persistAbsoluteFilePath = persistPath.absolutePath();
if (persistPath.isRelative()) {
// if the domain settings passed us a relative path, make an absolute path that is relative to the
// default data directory
persistAbsoluteFilePath = QDir(PathUtils::getAppDataFilePath("entities/")).absoluteFilePath(_persistFilePath);
_persistAbsoluteFilePath = QDir(PathUtils::getAppDataFilePath("entities/")).absoluteFilePath(_persistFilePath);
}
static const QString ENTITY_PERSIST_EXTENSION = ".json.gz";
// force the persist file to end with .json.gz
if (!persistAbsoluteFilePath.endsWith(ENTITY_PERSIST_EXTENSION, Qt::CaseInsensitive)) {
persistAbsoluteFilePath += ENTITY_PERSIST_EXTENSION;
if (!_persistAbsoluteFilePath.endsWith(ENTITY_PERSIST_EXTENSION, Qt::CaseInsensitive)) {
_persistAbsoluteFilePath += ENTITY_PERSIST_EXTENSION;
} else {
// make sure the casing of .json.gz is correct
persistAbsoluteFilePath.replace(ENTITY_PERSIST_EXTENSION, ENTITY_PERSIST_EXTENSION, Qt::CaseInsensitive);
_persistAbsoluteFilePath.replace(ENTITY_PERSIST_EXTENSION, ENTITY_PERSIST_EXTENSION, Qt::CaseInsensitive);
}
if (!QFile::exists(persistAbsoluteFilePath)) {
if (!QFile::exists(_persistAbsoluteFilePath)) {
qDebug() << "Persist file does not exist, checking for existence of persist file next to application";
static const QString OLD_DEFAULT_PERSIST_FILENAME = "resources/models.json.gz";
@ -1217,7 +1271,7 @@ void OctreeServer::domainSettingsRequestComplete() {
pathToCopyFrom = oldDefaultPersistPath;
}
QDir persistFileDirectory { QDir::cleanPath(persistAbsoluteFilePath + "/..") };
QDir persistFileDirectory { QDir::cleanPath(_persistAbsoluteFilePath + "/..") };
if (!persistFileDirectory.exists()) {
qDebug() << "Creating data directory " << persistFileDirectory.absolutePath();
@ -1225,15 +1279,15 @@ void OctreeServer::domainSettingsRequestComplete() {
}
if (shouldCopy) {
qDebug() << "Old persist file found, copying from " << pathToCopyFrom << " to " << persistAbsoluteFilePath;
qDebug() << "Old persist file found, copying from " << pathToCopyFrom << " to " << _persistAbsoluteFilePath;
QFile::copy(pathToCopyFrom, persistAbsoluteFilePath);
QFile::copy(pathToCopyFrom, _persistAbsoluteFilePath);
} else {
qDebug() << "No existing persist file found";
}
}
auto persistFileDirectory = QFileInfo(persistAbsoluteFilePath).absolutePath();
auto persistFileDirectory = QFileInfo(_persistAbsoluteFilePath).absolutePath();
if (_backupDirectoryPath.isEmpty()) {
// Use the persist file's directory to store backups
_backupDirectoryPath = persistFileDirectory;
@ -1264,7 +1318,7 @@ void OctreeServer::domainSettingsRequestComplete() {
qDebug() << "Backups will be stored in: " << _backupDirectoryPath;
// now set up PersistThread
_persistThread = new OctreePersistThread(_tree, persistAbsoluteFilePath, _backupDirectoryPath, _persistInterval,
_persistThread = new OctreePersistThread(_tree, _persistAbsoluteFilePath, _backupDirectoryPath, _persistInterval,
_wantBackup, _settings, _debugTimestampNow, _persistAsFileType);
_persistThread->initialize(true);
}

View file

@ -136,6 +136,7 @@ private slots:
void handleOctreeQueryPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode);
void handleOctreeDataNackPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode);
void handleJurisdictionRequestPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode);
void handleOctreeFileReplacement(QSharedPointer<ReceivedMessage> message);
void removeSendThread();
protected:
@ -172,6 +173,7 @@ protected:
QString _statusHost;
QString _persistFilePath;
QString _persistAbsoluteFilePath;
QString _persistAsFileType;
QString _backupDirectoryPath;
int _packetsPerClientPerInterval;

View file

@ -0,0 +1,41 @@
<!--#include virtual="header.html"-->
<div class="col-md-10 col-md-offset-1">
<div class="row">
<div class="col-xs-12">
<div class="alert" style="display:none;"></div>
</div>
</div>
<div class="row">
<div class="col-xs-12">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Upload Entities File</h3>
</div>
<form id="upload-form" action="upload" enctype="multipart/form-data" method="post">
<div class="panel-body">
<p>
Upload an entities file (e.g.: models.json.gz) to replace the content of this domain.</br>
Note: <strong>Your domain's content will be replaced by the content you upload</strong>, but the backup files of your domain's content will not immediately be changed.
</p>
<p>
If your domain has any content that you would like to re-use at a later date, save a manual backup of your models.json.gz file, which is usually stored in <i>C:\Users\[user_name]\AppData\Roaming\High Fidelity\assignment-client</i>.
</p>
<input type="file" name="entities-file" class="form-control-file" accept=".json, .gz">
</div>
<div class="panel-footer">
<input type="submit" class="btn btn-info" value="Upload">
</div>
</form>
</div>
</div>
</div>
</div>
<!--#include virtual="footer.html"-->
<script src='js/content.js'></script>
<script src='/js/sweetalert.min.js'></script>
<!--#include virtual="page-end.html"-->

View file

@ -0,0 +1,45 @@
$(document).ready(function(){
function showSpinnerAlert(title) {
swal({
title: title,
text: '<div class="spinner" style="color:black;"><div class="bounce1"></div><div class="bounce2"></div><div class="bounce3"></div></div>',
html: true,
showConfirmButton: false,
allowEscapeKey: false
});
}
var frm = $('#upload-form');
frm.submit(function (ev) {
$.ajax({
type: frm.attr('method'),
url: frm.attr('action'),
data: new FormData($(this)[0]),
cache: false,
contentType: false,
processData: false,
success: function (data) {
swal({
title: 'Uploaded',
type: 'success',
text: 'Your Entity Server is restarting to replace its local content with the uploaded file.',
confirmButtonText: 'OK'
})
},
error: function (data) {
swal({
title: '',
type: 'error',
text: 'Your entities file could not be transferred to the Entity Server.</br>Verify that the file is a <i>.json</i> or <i>.json.gz</i> entities file and try again.',
html: true,
confirmButtonText: 'OK',
});
}
});
ev.preventDefault();
showSpinnerAlert("Uploading Entities File");
});
});

View file

@ -36,6 +36,7 @@
<li><a href="/assignment">New Assignment</a></li>
</ul>
</li>
<li><a href="/content/">Content</a></li>
<li><a href="/settings/">Settings</a></li>
</ul>
</div>

View file

@ -99,7 +99,7 @@
<script src='/js/underscore-keypath.min.js'></script>
<script src='/js/bootbox.min.js'></script>
<script src='js/bootstrap-switch.min.js'></script>
<script src='js/sweetalert.min.js'></script>
<script src='/js/sweetalert.min.js'></script>
<script src='js/settings.js'></script>
<script src='js/form2js.min.js'></script>
<script src='js/sha256.js'></script>

View file

@ -1633,6 +1633,15 @@ QString pathForAssignmentScript(const QUuid& assignmentUUID) {
return directory.absoluteFilePath(uuidStringWithoutCurlyBraces(assignmentUUID));
}
QString DomainServer::pathForRedirect(QString path) const {
// make sure the passed path has a leading slash
if (!path.startsWith('/')) {
path.insert(0, '/');
}
return "http://" + _hostname + ":" + QString::number(_httpManager.serverPort()) + path;
}
const QString URI_OAUTH = "/oauth";
bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url, bool skipSubHandler) {
const QString JSON_MIME_TYPE = "application/json";
@ -1640,6 +1649,7 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url
const QString URI_ASSIGNMENT = "/assignment";
const QString URI_NODES = "/nodes";
const QString URI_SETTINGS = "/settings";
const QString URI_ENTITY_FILE_UPLOAD = "/content/upload";
const QString UUID_REGEX_STRING = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
@ -1869,6 +1879,25 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url
// respond with a 200 code for successful upload
connection->respond(HTTPConnection::StatusCode200);
return true;
} else if (url.path() == URI_ENTITY_FILE_UPLOAD) {
// this is an entity file upload, ask the HTTPConnection to parse the data
QList<FormData> formData = connection->parseFormData();
Headers redirectHeaders;
if (formData.size() > 0 && formData[0].second.size() > 0) {
// invoke our method to hand the new octree file off to the octree server
QMetaObject::invokeMethod(this, "handleOctreeFileReplacement",
Qt::QueuedConnection, Q_ARG(QByteArray, formData[0].second));
// respond with a 200 for success
connection->respond(HTTPConnection::StatusCode200);
} else {
// respond with a 400 for failure
connection->respond(HTTPConnection::StatusCode400);
}
return true;
}
} else if (connection->requestOperation() == QNetworkAccessManager::DeleteOperation) {
@ -2159,8 +2188,7 @@ Headers DomainServer::setupCookieHeadersFromProfileReply(QNetworkReply* profileR
cookieHeaders.insert("Set-Cookie", cookieString.toUtf8());
// redirect the user back to the homepage so they can present their cookie and be authenticated
QString redirectString = "http://" + _hostname + ":" + QString::number(_httpManager.serverPort());
cookieHeaders.insert("Location", redirectString.toUtf8());
cookieHeaders.insert("Location", pathForRedirect().toUtf8());
return cookieHeaders;
}
@ -2560,3 +2588,20 @@ void DomainServer::setupGroupCacheRefresh() {
_metaverseGroupCacheTimer->start(REFRESH_GROUPS_INTERVAL_MSECS);
}
}
void DomainServer::handleOctreeFileReplacement(QByteArray octreeFile) {
// enumerate the nodes and find any octree type servers with active sockets
auto limitedNodeList = DependencyManager::get<LimitedNodeList>();
limitedNodeList->eachMatchingNode([](const SharedNodePointer& node){
return node->getType() == NodeType::EntityServer && node->getActiveSocket();
}, [&octreeFile, limitedNodeList](const SharedNodePointer& octreeNode){
// setup a packet to send to this octree server with the new octree file data
auto octreeFilePacketList = NLPacketList::create(PacketType::OctreeFileReplacement, QByteArray(), true, true);
octreeFilePacketList->write(octreeFile);
qDebug() << "Sending an octree file replacement of" << octreeFile.size() << "bytes to" << octreeNode;
limitedNodeList->sendPacketList(std::move(octreeFilePacketList), *octreeNode);
});
}

View file

@ -100,6 +100,8 @@ private slots:
void handleSuccessfulICEServerAddressUpdate(QNetworkReply& requestReply);
void handleFailedICEServerAddressUpdate(QNetworkReply& requestReply);
void handleOctreeFileReplacement(QByteArray octreeFile);
signals:
void iceServerChanged();
void userConnected();
@ -161,6 +163,8 @@ private:
void setupGroupCacheRefresh();
QString pathForRedirect(QString path = QString()) const;
SubnetList _acSubnetWhitelist;
DomainGatekeeper _gatekeeper;

View file

@ -39,7 +39,7 @@ const QSet<PacketType> NON_SOURCED_PACKETS = QSet<PacketType>()
<< PacketType::ICEServerPeerInformation << PacketType::ICEServerQuery << PacketType::ICEServerHeartbeat
<< PacketType::ICEServerHeartbeatACK << PacketType::ICEPing << PacketType::ICEPingReply
<< PacketType::ICEServerHeartbeatDenied << PacketType::AssignmentClientStatus << PacketType::StopNode
<< PacketType::DomainServerRemovedNode << PacketType::UsernameFromIDReply;
<< PacketType::DomainServerRemovedNode << PacketType::UsernameFromIDReply << PacketType::OctreeFileReplacement;
PacketVersion versionForPacketType(PacketType packetType) {
switch (packetType) {

View file

@ -113,7 +113,8 @@ public:
EntityPhysics,
EntityServerScriptLog,
AdjustAvatarSorting,
LAST_PACKET_TYPE = AdjustAvatarSorting
OctreeFileReplacement,
LAST_PACKET_TYPE = OctreeFileReplacement
};
};

View file

@ -33,6 +33,7 @@
#include "OctreePersistThread.h"
const int OctreePersistThread::DEFAULT_PERSIST_INTERVAL = 1000 * 30; // every 30 seconds
const QString OctreePersistThread::REPLACEMENT_FILE_EXTENSION = ".replace";
OctreePersistThread::OctreePersistThread(OctreePointer tree, const QString& filename, const QString& backupDirectory, int persistInterval,
bool wantBackup, const QJsonObject& settings, bool debugTimestampNow,
@ -131,10 +132,46 @@ quint64 OctreePersistThread::getMostRecentBackupTimeInUsecs(const QString& forma
return mostRecentBackupInUsecs;
}
void OctreePersistThread::possiblyReplaceContent() {
// before we load the normal file, check if there's a pending replacement file
auto replacementFileName = _filename + REPLACEMENT_FILE_EXTENSION;
QFile replacementFile { replacementFileName };
if (replacementFile.exists()) {
// we have a replacement file to process
qDebug() << "Replacing models file with" << replacementFileName;
// first take the current models file and move it to a different filename, appended with the timestamp
QFile currentFile { _filename };
if (currentFile.exists()) {
static const QString FILENAME_TIMESTAMP_FORMAT = "yyyyMMdd-hhmmss";
auto backupFileName = _filename + ".backup." + QDateTime::currentDateTime().toString(FILENAME_TIMESTAMP_FORMAT);
if (currentFile.rename(backupFileName)) {
qDebug() << "Moved previous models file to" << backupFileName;
} else {
qWarning() << "Could not backup previous models file to" << backupFileName << "- removing replacement models file";
if (!QFile::remove(replacementFileName)) {
qWarning() << "Could not remove replacement models file from" << replacementFileName
<< "- replacement will be re-attempted on next server restart";
}
}
}
// rename the replacement file to match what the persist thread is just about to read
if (!replacementFile.rename(_filename)) {
qWarning() << "Could not replace models file with" << replacementFileName << "- starting with empty models file";
}
}
}
bool OctreePersistThread::process() {
if (!_initialLoadComplete) {
possiblyReplaceContent();
quint64 loadStarted = usecTimestampNow();
qCDebug(octree) << "loading Octrees from file: " << _filename << "...";

View file

@ -32,6 +32,7 @@ public:
};
static const int DEFAULT_PERSIST_INTERVAL;
static const QString REPLACEMENT_FILE_EXTENSION;
OctreePersistThread(OctreePointer tree, const QString& filename, const QString& backupDirectory,
int persistInterval = DEFAULT_PERSIST_INTERVAL, bool wantBackup = false,
@ -60,6 +61,7 @@ protected:
bool getMostRecentBackup(const QString& format, QString& mostRecentBackupFileName, QDateTime& mostRecentBackupTime);
quint64 getMostRecentBackupTimeInUsecs(const QString& format);
void parseSettings(const QJsonObject& settings);
void possiblyReplaceContent();
private:
OctreePointer _tree;