Handle possibly raised exceptions in RecoveryRunnable::run(). Since the run() method is executed within the context of a worker thread all exceptions must be handled before returning to Qt Concurrent as otherwise the application will be terminated. For testing purposes load the corrupted project file from this forum thread https://forum.freecad.org/viewtopic.php?p=823608#p823608 and wait for the auto-saving.
419 lines
14 KiB
C++
419 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 "PreCompiled.h"
|
|
|
|
#ifndef _PreComp_
|
|
# include <QApplication>
|
|
# include <QFile>
|
|
# include <QDir>
|
|
# include <QRunnable>
|
|
# include <QTextStream>
|
|
# include <QThreadPool>
|
|
#endif
|
|
|
|
#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("Please wait until the AutoRecovery 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 AutoRecovery 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"
|