Merge branch 'master' of https://github.com/highfidelity/hifi into 21274

This commit is contained in:
Olivier Prat 2017-04-30 13:07:26 +02:00
commit 14b2b98db6
18 changed files with 281 additions and 63 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,44 @@
<!--#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 at the following paths:<br>
<pre>C:\Users\[username]\AppData\Roaming\High Fidelity\assignment-client/entities/models.json.gz</pre>
<pre>/Users/[username]/Library/Application Support/High Fidelity/assignment-client/entities/models.json.gz</pre>
<pre>/home/[username]/.local/share/High Fidelity/assignment-client/entities/models.json.gz</pre>
</p>
<br>
<input type="file" name="entities-file" class="form-control-file" accept=".json, .gz">
<br>
</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

@ -798,7 +798,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
connect(&domainHandler, SIGNAL(resetting()), SLOT(resettingDomain()));
connect(&domainHandler, SIGNAL(connectedToDomain(const QString&)), SLOT(updateWindowTitle()));
connect(&domainHandler, SIGNAL(disconnectedFromDomain()), SLOT(updateWindowTitle()));
connect(&domainHandler, SIGNAL(disconnectedFromDomain()), SLOT(clearDomainOctreeDetails()));
connect(&domainHandler, &DomainHandler::disconnectedFromDomain, this, &Application::clearDomainAvatars);
connect(&domainHandler, &DomainHandler::disconnectedFromDomain, this, [this]() {
getOverlays().deleteOverlay(getTabletScreenID());
getOverlays().deleteOverlay(getTabletHomeButtonID());
@ -5179,7 +5179,6 @@ void Application::clearDomainOctreeDetails() {
qCDebug(interfaceapp) << "Clearing domain octree details...";
resetPhysicsReadyInformation();
getMyAvatar()->setAvatarEntityDataChanged(true); // to recreate worn entities
// reset our node to stats and node to jurisdiction maps... since these must be changing...
_entityServerJurisdictions.withWriteLock([&] {
@ -5198,14 +5197,18 @@ void Application::clearDomainOctreeDetails() {
skyStage->setBackgroundMode(model::SunSkyStage::SKY_DEFAULT);
_recentlyClearedDomain = true;
DependencyManager::get<AvatarManager>()->clearOtherAvatars();
DependencyManager::get<AnimationCache>()->clearUnusedResources();
DependencyManager::get<ModelCache>()->clearUnusedResources();
DependencyManager::get<SoundCache>()->clearUnusedResources();
DependencyManager::get<TextureCache>()->clearUnusedResources();
}
void Application::clearDomainAvatars() {
getMyAvatar()->setAvatarEntityDataChanged(true); // to recreate worn entities
DependencyManager::get<AvatarManager>()->clearOtherAvatars();
}
void Application::domainChanged(const QString& domainHostname) {
updateWindowTitle();
// disable physics until we have enough information about our new location to not cause craziness.
@ -5275,33 +5278,8 @@ void Application::nodeKilled(SharedNodePointer node) {
if (node->getType() == NodeType::AudioMixer) {
QMetaObject::invokeMethod(DependencyManager::get<AudioClient>().data(), "audioMixerKilled");
} else if (node->getType() == NodeType::EntityServer) {
QUuid nodeUUID = node->getUUID();
// see if this is the first we've heard of this node...
_entityServerJurisdictions.withReadLock([&] {
if (_entityServerJurisdictions.find(nodeUUID) == _entityServerJurisdictions.end()) {
return;
}
auto rootCode = _entityServerJurisdictions[nodeUUID].getRootOctalCode();
VoxelPositionSize rootDetails;
voxelDetailsForCode(rootCode.get(), rootDetails);
qCDebug(interfaceapp, "model server going away...... v[%f, %f, %f, %f]",
(double)rootDetails.x, (double)rootDetails.y, (double)rootDetails.z, (double)rootDetails.s);
});
// If the model server is going away, remove it from our jurisdiction map so we don't send voxels to a dead server
_entityServerJurisdictions.withWriteLock([&] {
_entityServerJurisdictions.erase(_entityServerJurisdictions.find(nodeUUID));
});
// also clean up scene stats for that server
_octreeServerSceneStats.withWriteLock([&] {
if (_octreeServerSceneStats.find(nodeUUID) != _octreeServerSceneStats.end()) {
_octreeServerSceneStats.erase(nodeUUID);
}
});
// we lost an entity server, clear all of the domain octree details
clearDomainOctreeDetails();
} else if (node->getType() == NodeType::AvatarMixer) {
// our avatar mixer has gone away - clear the hash of avatars
DependencyManager::get<AvatarManager>()->clearOtherAvatars();

View file

@ -409,6 +409,7 @@ public slots:
private slots:
void showDesktop();
void clearDomainOctreeDetails();
void clearDomainAvatars();
void aboutToQuit();
void resettingDomain();

View file

@ -49,20 +49,20 @@
An dynamic is a callback which is registered with bullet. An dynamic is called-back every physics
A dynamic is a callback which is registered with bullet. A dynamic is called-back every physics
simulation step and can do whatever it wants with the various datastructures it has available. An
dynamic, for example, can pull an EntityItem toward a point as if that EntityItem were connected to that
point by a spring.
In this system, an dynamic is a property of an EntityItem (rather, an EntityItem has a property which
In this system, a dynamic is a property of an EntityItem (rather, an EntityItem has a property which
encodes a list of dynamics). Each dynamic has a type and some arguments. Dynamics can be created by a
script or when receiving information via an EntityTree data-stream (either over the network or from an
svo file).
In the interface, if an EntityItem has dynamics, this EntityItem will have pointers to ObjectDynamic
subclass (like ObjectDynamicSpring) instantiations. Code in the entities library affects an dynamic-object
subclass (like ObjectDynamicSpring) instantiations. Code in the entities library affects a dynamic-object
via the EntityDynamicInterface (which knows nothing about bullet). When the ObjectDynamic subclass
instance is created, it is registered as an dynamic with bullet. Bullet will call into code in this
instance is created, it is registered as a dynamic with bullet. Bullet will call into code in this
instance with the btDynamicInterface every physics-simulation step.
Because the dynamic can exist next to the interface's EntityTree or the entity-server's EntityTree,

View file

@ -348,18 +348,19 @@ void AssetClient::handleAssetGetReply(QSharedPointer<ReceivedMessage> message, S
// Store message in case we need to disconnect from it later.
callbacks.message = message;
auto weakNode = senderNode.toWeakRef();
connect(message.data(), &ReceivedMessage::progress, this, [this, weakNode, messageID, length](qint64 size) {
handleProgressCallback(weakNode, messageID, size, length);
});
connect(message.data(), &ReceivedMessage::completed, this, [this, weakNode, messageID]() {
handleCompleteCallback(weakNode, messageID);
});
if (message->isComplete()) {
disconnect(message.data(), nullptr, this, nullptr);
callbacks.completeCallback(true, error, message->readAll());
messageCallbackMap.erase(requestIt);
} else {
auto weakNode = senderNode.toWeakRef();
connect(message.data(), &ReceivedMessage::progress, this, [this, weakNode, messageID, length](qint64 size) {
handleProgressCallback(weakNode, messageID, size, length);
});
connect(message.data(), &ReceivedMessage::completed, this, [this, weakNode, messageID]() {
handleCompleteCallback(weakNode, messageID);
});
}
}

View file

@ -77,7 +77,7 @@ void AssetRequest::start() {
_assetRequestID = assetClient->getAsset(_hash, _byteRange.fromInclusive, _byteRange.toExclusive,
[this, that, hash](bool responseReceived, AssetServerError serverError, const QByteArray& data) {
if (!that) {
if (!that) {
qCWarning(asset_client) << "Got reply for dead asset request " << hash << "- error code" << _error;
// If the request is dead, return
return;
@ -113,8 +113,10 @@ void AssetRequest::start() {
_data = data;
_totalReceived += data.size();
emit progress(_totalReceived, data.size());
saveToCache(getUrl(), data);
if (!_byteRange.isSet()) {
saveToCache(getUrl(), data);
}
}
}

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,47 @@ 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 (!replacementFile.remove()) {
qWarning() << "Could not remove replacement models file from" << replacementFileName
<< "- replacement will be re-attempted on next server restart";
return;
}
}
}
// 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;