/*************************************************************************** * Copyright (c) 2015 Werner Mayer * * * * 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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(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::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 << "\n" << "\n"; str << " Created\n"; str << " \n"; // store the document's current label str << " " << QString::fromUtf8(doc->FileName.getValue()) << "\n"; // store the document's current filename str << "\n"; file.close(); } // make sure to tmp. disable saving thumbnails because this causes trouble if the // associated 3d view is not active Base::Reference 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(doc)->signalNewObject.connect( std::bind(&AutoSaveProperty::slotNewObject, this, sp::_1) ); documentMod = const_cast(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 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(&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()) { const auto* prop = static_cast(object); const App::PropertyContainer* parent = prop->getContainer(); if (parent && parent->isDerivedFrom()) { return true; } } else if (object->isDerivedFrom()) { return true; } // These are the addresses of touched properties of a document object. std::stringstream str; str << static_cast(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::iterator it = saver.fileMap.find(address); if (it == saver.fileMap.end() || it->second != name) { saver.fileMap[address] = name; return true; } std::set::const_iterator jt = saver.touched.find(address); return (jt != saver.touched.end()); } namespace Gui { class RecoveryRunnable: public QRunnable { public: RecoveryRunnable( const std::set& 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; }; } // namespace Gui 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()) { const auto* prop = static_cast(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"