"Professional CMake" book suggest the following: "Targets should build successfully with or without compiler support for precompiled headers. It should be considered an optimization, not a requirement. In particular, do not explicitly include a precompile header (e.g. stdafx.h) in the source code, let CMake force-include an automatically generated precompile header on the compiler command line instead. This is more portable across the major compilers and is likely to be easier to maintain. It will also avoid warnings being generated from certain code checking tools like iwyu (include what you use)." Therefore, removed the "#include <PreCompiled.h>" from sources, also there is no need for the "#ifdef _PreComp_" anymore
414 lines
14 KiB
C++
414 lines
14 KiB
C++
/***************************************************************************
|
|
* Copyright (c) 2015 Werner Mayer <wmayer[at]users.sourceforge.net> *
|
|
* *
|
|
* This file is part of the FreeCAD CAx development system. *
|
|
* *
|
|
* This library is free software; you can redistribute it and/or *
|
|
* modify it under the terms of the GNU Library General Public *
|
|
* License as published by the Free Software Foundation; either *
|
|
* version 2 of the License, or (at your option) any later version. *
|
|
* *
|
|
* This library is distributed in the hope that it will be useful, *
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
|
* GNU Library General Public License for more details. *
|
|
* *
|
|
* You should have received a copy of the GNU Library General Public *
|
|
* License along with this library; see the file COPYING.LIB. If not, *
|
|
* write to the Free Software Foundation, Inc., 59 Temple Place, *
|
|
* Suite 330, Boston, MA 02111-1307, USA *
|
|
* *
|
|
***************************************************************************/
|
|
|
|
# include <QApplication>
|
|
# include <QFile>
|
|
# include <QDir>
|
|
# include <QRunnable>
|
|
# include <QTextStream>
|
|
# include <QThreadPool>
|
|
|
|
#include <App/Application.h>
|
|
#include <App/Document.h>
|
|
#include <App/DocumentObject.h>
|
|
#include <Base/Console.h>
|
|
#include <Base/FileInfo.h>
|
|
#include <Base/Stream.h>
|
|
#include <Base/TimeInfo.h>
|
|
#include <Base/Tools.h>
|
|
#include <Base/Writer.h>
|
|
|
|
#include "AutoSaver.h"
|
|
#include "Document.h"
|
|
#include "MainWindow.h"
|
|
#include "ViewProvider.h"
|
|
#include "WaitCursor.h"
|
|
|
|
FC_LOG_LEVEL_INIT("App",true,true)
|
|
|
|
using namespace Gui;
|
|
namespace sp = std::placeholders;
|
|
|
|
AutoSaver* AutoSaver::self = nullptr;
|
|
const int AutoSaveTimeout = 900000;
|
|
|
|
AutoSaver::AutoSaver(QObject* parent)
|
|
: QObject(parent)
|
|
, timeout(AutoSaveTimeout)
|
|
, compressed(true)
|
|
{
|
|
//NOLINTBEGIN
|
|
App::GetApplication().signalNewDocument.connect(std::bind(&AutoSaver::slotCreateDocument, this, sp::_1));
|
|
App::GetApplication().signalDeleteDocument.connect(std::bind(&AutoSaver::slotDeleteDocument, this, sp::_1));
|
|
//NOLINTEND
|
|
}
|
|
|
|
AutoSaver::~AutoSaver() = default;
|
|
|
|
AutoSaver* AutoSaver::instance()
|
|
{
|
|
if (!self) {
|
|
self = new AutoSaver(QApplication::instance());
|
|
}
|
|
return self;
|
|
}
|
|
|
|
void AutoSaver::renameFile(QString dirName, QString file, QString tmpFile)
|
|
{
|
|
FC_LOG("auto saver rename " << tmpFile.toUtf8().constData()
|
|
<< " -> " << file.toUtf8().constData());
|
|
QDir dir(dirName);
|
|
dir.remove(file);
|
|
if (!dir.rename(tmpFile,file)) {
|
|
FC_ERR("Failed to rename autosave file " << tmpFile.toStdString() << " to " << file.toStdString() << "\n");
|
|
}
|
|
}
|
|
|
|
void AutoSaver::setTimeout(int ms)
|
|
{
|
|
timeout = Base::clamp<int>(ms, 0, 3600000); // between 0 and 60 min
|
|
|
|
// go through the attached documents and apply the new timeout
|
|
for (auto & it : saverMap) {
|
|
if (it.second->timerId > 0)
|
|
killTimer(it.second->timerId);
|
|
int id = timeout > 0 ? startTimer(timeout) : 0;
|
|
it.second->timerId = id;
|
|
}
|
|
}
|
|
|
|
void AutoSaver::setCompressed(bool on)
|
|
{
|
|
this->compressed = on;
|
|
}
|
|
|
|
void AutoSaver::slotCreateDocument(const App::Document& Doc)
|
|
{
|
|
std::string name = Doc.getName();
|
|
int id = timeout > 0 ? startTimer(timeout) : 0;
|
|
AutoSaveProperty* as = new AutoSaveProperty(&Doc);
|
|
as->timerId = id;
|
|
|
|
if (!this->compressed) {
|
|
std::string dirName = Doc.TransientDir.getValue();
|
|
dirName += "/fc_recovery_files";
|
|
Base::FileInfo fi(dirName);
|
|
fi.createDirectory();
|
|
as->dirName = dirName;
|
|
}
|
|
saverMap.insert(std::make_pair(name, as));
|
|
}
|
|
|
|
void AutoSaver::slotDeleteDocument(const App::Document& Doc)
|
|
{
|
|
std::string name = Doc.getName();
|
|
std::map<std::string, AutoSaveProperty*>::iterator it = saverMap.find(name);
|
|
if (it != saverMap.end()) {
|
|
if (it->second->timerId > 0)
|
|
killTimer(it->second->timerId);
|
|
delete it->second;
|
|
saverMap.erase(it);
|
|
}
|
|
}
|
|
|
|
void AutoSaver::saveDocument(const std::string& name, AutoSaveProperty& saver)
|
|
{
|
|
Gui::WaitCursor wc;
|
|
App::Document* doc = App::GetApplication().getDocument(name.c_str());
|
|
if (doc && !doc->testStatus(App::Document::PartialDoc)
|
|
&& !doc->testStatus(App::Document::TempDoc))
|
|
{
|
|
// Set the document's current transient directory
|
|
std::string dirName = doc->TransientDir.getValue();
|
|
dirName += "/fc_recovery_files";
|
|
saver.dirName = dirName;
|
|
|
|
// Write recovery meta file
|
|
QFile file(QStringLiteral("%1/fc_recovery_file.xml")
|
|
.arg(QString::fromUtf8(doc->TransientDir.getValue())));
|
|
if (file.open(QFile::WriteOnly)) {
|
|
QTextStream str(&file);
|
|
#if QT_VERSION < QT_VERSION_CHECK(6,0,0)
|
|
str.setCodec("UTF-8");
|
|
#endif
|
|
str << "<?xml version='1.0' encoding='utf-8'?>\n"
|
|
<< "<AutoRecovery SchemaVersion=\"1\">\n";
|
|
str << " <Status>Created</Status>\n";
|
|
str << " <Label>" << QString::fromUtf8(doc->Label.getValue()) << "</Label>\n"; // store the document's current label
|
|
str << " <FileName>" << QString::fromUtf8(doc->FileName.getValue()) << "</FileName>\n"; // store the document's current filename
|
|
str << "</AutoRecovery>\n";
|
|
file.close();
|
|
}
|
|
|
|
// make sure to tmp. disable saving thumbnails because this causes trouble if the
|
|
// associated 3d view is not active
|
|
Base::Reference<ParameterGrp> hGrp = App::GetApplication().GetParameterGroupByPath
|
|
("User parameter:BaseApp/Preferences/Document");
|
|
bool save = hGrp->GetBool("SaveThumbnail",true);
|
|
hGrp->SetBool("SaveThumbnail",false);
|
|
|
|
getMainWindow()->showMessage(tr("Wait until the auto-recovery file has been saved…"), 5000);
|
|
//qApp->processEvents();
|
|
|
|
Base::TimeElapsed startTime;
|
|
// open extra scope to close ZipWriter properly
|
|
{
|
|
if (!this->compressed) {
|
|
RecoveryWriter writer(saver);
|
|
|
|
// We will be using thread pool if not compressed.
|
|
// So, always force binary format because ASCII
|
|
// is not reentrant. See PropertyPartShape::SaveDocFile
|
|
writer.setMode("BinaryBrep");
|
|
|
|
writer.putNextEntry("Document.xml");
|
|
|
|
doc->Save(writer);
|
|
|
|
// Special handling for Gui document.
|
|
doc->signalSaveDocument(writer);
|
|
|
|
// write additional files
|
|
writer.writeFiles();
|
|
}
|
|
// only create the file if something has changed
|
|
else if (!saver.touched.empty()) {
|
|
std::string fn = doc->TransientDir.getValue();
|
|
fn += "/fc_recovery_file.fcstd";
|
|
Base::FileInfo tmp(fn);
|
|
Base::ofstream file(tmp, std::ios::out | std::ios::binary);
|
|
if (file.is_open())
|
|
{
|
|
Base::ZipWriter writer(file);
|
|
if (hGrp->GetBool("SaveBinaryBrep", true))
|
|
writer.setMode("BinaryBrep");
|
|
|
|
writer.setComment("AutoRecovery file");
|
|
writer.setLevel(1); // apparently the fastest compression
|
|
writer.putNextEntry("Document.xml");
|
|
|
|
doc->Save(writer);
|
|
|
|
// Special handling for Gui document.
|
|
doc->signalSaveDocument(writer);
|
|
|
|
// write additional files
|
|
writer.writeFiles();
|
|
}
|
|
}
|
|
}
|
|
|
|
Base::Console().log("Save auto-recovery file in %fs\n", Base::TimeElapsed::diffTimeF(startTime,Base::TimeElapsed()));
|
|
hGrp->SetBool("SaveThumbnail",save);
|
|
}
|
|
}
|
|
|
|
void AutoSaver::timerEvent(QTimerEvent * event)
|
|
{
|
|
int id = event->timerId();
|
|
for (auto & it : saverMap) {
|
|
if (it.second->timerId == id) {
|
|
try {
|
|
saveDocument(it.first, *it.second);
|
|
it.second->touched.clear();
|
|
break;
|
|
}
|
|
catch (...) {
|
|
Base::Console().error("Failed to auto-save document '%s'\n", it.first.c_str());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
|
|
AutoSaveProperty::AutoSaveProperty(const App::Document* doc) : timerId(-1)
|
|
{
|
|
//NOLINTBEGIN
|
|
documentNew = const_cast<App::Document*>(doc)->signalNewObject.connect
|
|
(std::bind(&AutoSaveProperty::slotNewObject, this, sp::_1));
|
|
documentMod = const_cast<App::Document*>(doc)->signalChangedObject.connect
|
|
(std::bind(&AutoSaveProperty::slotChangePropertyData, this, sp::_2));
|
|
//NOLINTEND
|
|
}
|
|
|
|
AutoSaveProperty::~AutoSaveProperty()
|
|
{
|
|
documentNew.disconnect();
|
|
documentMod.disconnect();
|
|
}
|
|
|
|
void AutoSaveProperty::slotNewObject(const App::DocumentObject& obj)
|
|
{
|
|
std::vector<App::Property*> props;
|
|
obj.getPropertyList(props);
|
|
|
|
// if an object was deleted and then restored by an undo then add all properties
|
|
// because this might be the data files which we may want to re-write
|
|
for (const auto & prop : props) {
|
|
slotChangePropertyData(*prop);
|
|
}
|
|
}
|
|
|
|
void AutoSaveProperty::slotChangePropertyData(const App::Property& prop)
|
|
{
|
|
std::stringstream str;
|
|
str << static_cast<const void *>(&prop) << std::ends;
|
|
std::string address = str.str();
|
|
this->touched.insert(address);
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
|
|
RecoveryWriter::RecoveryWriter(AutoSaveProperty& saver)
|
|
: Base::FileWriter(saver.dirName.c_str()), saver(saver)
|
|
{
|
|
}
|
|
|
|
RecoveryWriter::~RecoveryWriter() = default;
|
|
|
|
bool RecoveryWriter::shouldWrite(const std::string& name, const Base::Persistence *object) const
|
|
{
|
|
// Property files of a view provider can always be written because
|
|
// these are rather small files.
|
|
if (object->isDerivedFrom<App::Property>()) {
|
|
const auto* prop = static_cast<const App::Property*>(object);
|
|
const App::PropertyContainer* parent = prop->getContainer();
|
|
if (parent && parent->isDerivedFrom<Gui::ViewProvider>())
|
|
return true;
|
|
}
|
|
else if (object->isDerivedFrom<Gui::Document>()) {
|
|
return true;
|
|
}
|
|
|
|
// These are the addresses of touched properties of a document object.
|
|
std::stringstream str;
|
|
str << static_cast<const void *>(object) << std::ends;
|
|
std::string address = str.str();
|
|
|
|
// Check if the property will be exported to the same file. If the file has changed or if the property hasn't been
|
|
// yet exported then (re-)write the file.
|
|
std::map<std::string, std::string>::iterator it = saver.fileMap.find(address);
|
|
if (it == saver.fileMap.end() || it->second != name) {
|
|
saver.fileMap[address] = name;
|
|
return true;
|
|
}
|
|
|
|
std::set<std::string>::const_iterator jt = saver.touched.find(address);
|
|
return (jt != saver.touched.end());
|
|
}
|
|
|
|
namespace Gui {
|
|
|
|
class RecoveryRunnable : public QRunnable
|
|
{
|
|
public:
|
|
RecoveryRunnable(const std::set<std::string>& modes, const char* dir, const char* file, const App::Property* p)
|
|
: prop(p->Copy())
|
|
, writer(dir)
|
|
{
|
|
writer.setModes(modes);
|
|
|
|
dirName = QString::fromUtf8(dir);
|
|
fileName = QString::fromUtf8(file);
|
|
tmpName = QStringLiteral("%1.tmp%2").arg(fileName).arg(rand());
|
|
writer.putNextEntry(tmpName.toUtf8().constData());
|
|
}
|
|
~RecoveryRunnable() override
|
|
{
|
|
delete prop;
|
|
}
|
|
void run() override
|
|
{
|
|
try {
|
|
prop->SaveDocFile(writer);
|
|
writer.close();
|
|
|
|
// We could have renamed the file in this thread. However, there is
|
|
// still chance of crash when we deleted the original and before rename
|
|
// the new file. So we ask the main thread to do it. There is still
|
|
// possibility of crash caused by thread other than the main, but
|
|
// that's the best we can do for now.
|
|
QMetaObject::invokeMethod(AutoSaver::instance(), "renameFile",
|
|
Qt::QueuedConnection, Q_ARG(QString,dirName)
|
|
,Q_ARG(QString,fileName),Q_ARG(QString,tmpName));
|
|
}
|
|
catch (const Base::Exception& e) {
|
|
Base::Console().warning("Exception in auto-saving: %s\n", e.what());
|
|
}
|
|
catch (const std::exception& e) {
|
|
Base::Console().warning("C++ exception in auto-saving: %s\n", e.what());
|
|
}
|
|
catch (...) {
|
|
Base::Console().warning("Unknown exception in auto-saving\n");
|
|
}
|
|
}
|
|
|
|
private:
|
|
App::Property* prop;
|
|
Base::FileWriter writer;
|
|
QString dirName;
|
|
QString fileName;
|
|
QString tmpName;
|
|
};
|
|
|
|
}
|
|
|
|
void RecoveryWriter::writeFiles()
|
|
{
|
|
// use a while loop because it is possible that while
|
|
// processing the files new ones can be added
|
|
size_t index = 0;
|
|
this->FileStream.close();
|
|
while (index < FileList.size()) {
|
|
FileEntry entry = FileList.begin()[index];
|
|
|
|
if (shouldWrite(entry.FileName, entry.Object)) {
|
|
std::string filePath = entry.FileName;
|
|
std::string::size_type pos = 0;
|
|
while ((pos = filePath.find('/', pos)) != std::string::npos) {
|
|
std::string dirName = DirName + "/" + filePath.substr(0, pos);
|
|
pos++;
|
|
Base::FileInfo fi(dirName);
|
|
fi.createDirectory();
|
|
}
|
|
|
|
// For properties a copy can be created and then this can be written to disk in a thread
|
|
if (entry.Object->isDerivedFrom<App::Property>()) {
|
|
const auto* prop = static_cast<const App::Property*>(entry.Object);
|
|
QThreadPool::globalInstance()->start(new RecoveryRunnable(getModes(), DirName.c_str(), entry.FileName.c_str(), prop));
|
|
}
|
|
else {
|
|
std::string fileName = DirName + "/" + entry.FileName;
|
|
this->FileStream.open(fileName.c_str(), std::ios::out | std::ios::binary);
|
|
entry.Object->SaveDocFile(*this);
|
|
this->FileStream.close();
|
|
}
|
|
}
|
|
|
|
index++;
|
|
}
|
|
}
|
|
|
|
|
|
#include "moc_AutoSaver.cpp"
|