App: fix external document loading

The problem happens when partial loading is enabled. If document A
contains a link to some object in document B, it will load B as partial
document with only that object and its necessary dependencies. But if
document A contains another link to some object in document C which also
has a link to some object in document B, the link in document C may not
be restored, because document B is partially loaded without the linked
object. This patch will check for this case and reload document B for
more objects.

See an example reported in
https://forum.freecadweb.org/viewtopic.php?p=495078#p495078
This commit is contained in:
Zheng, Lei
2021-08-02 11:38:01 +08:00
parent 414ea71a28
commit fc9d3547ad
7 changed files with 311 additions and 166 deletions

View File

@@ -55,6 +55,8 @@
#include <sys/sysctl.h>
#endif
#include <boost/algorithm/string/predicate.hpp>
#include "Application.h"
#include "Document.h"
@@ -99,6 +101,7 @@
#include "Document.h"
#include "DocumentObjectGroup.h"
#include "DocumentObjectFileIncluded.h"
#include "DocumentObserver.h"
#include "InventorObject.h"
#include "VRMLObject.h"
#include "Annotation.h"
@@ -487,6 +490,7 @@ bool Application::closeDocument(const char* name)
setActiveDocument((Document*)0);
std::unique_ptr<Document> delDoc (pos->second);
DocMap.erase( pos );
DocFileMap.erase(FileInfo(delDoc->FileName.getValue()).filePath());
_objCount = -1;
@@ -566,8 +570,10 @@ int Application::addPendingDocument(const char *FileName, const char *objName, b
return -1;
assert(FileName && FileName[0]);
assert(objName && objName[0]);
auto ret = _pendingDocMap.emplace(FileName,std::set<std::string>());
ret.first->second.emplace(objName);
if(!_docReloadAttempts[FileName].emplace(objName).second)
return -1;
auto ret = _pendingDocMap.emplace(FileName,std::vector<std::string>());
ret.first->second.push_back(objName);
if(ret.second) {
_pendingDocs.push_back(ret.first->first.c_str());
return 1;
@@ -623,6 +629,41 @@ Document* Application::openDocument(const char * FileName, bool createView) {
return 0;
}
Document *Application::getDocumentByPath(const char *path, int checkCanonical) const {
if(!path || !path[0])
return nullptr;
if(DocFileMap.empty()) {
for(auto &v : DocMap) {
const auto &file = v.second->FileName.getStrValue();
if(file.size())
DocFileMap[FileInfo(file.c_str()).filePath()] = v.second;
}
}
auto it = DocFileMap.find(FileInfo(path).filePath());
if(it != DocFileMap.end())
return it->second;
if (!checkCanonical)
return nullptr;
std::string filepath = FileInfo(path).filePath();
QString canonicalPath = QFileInfo(QString::fromUtf8(path)).canonicalFilePath();
for (auto &v : DocMap) {
QFileInfo fi(QString::fromUtf8(v.second->FileName.getValue()));
if (canonicalPath == fi.canonicalFilePath()) {
if (checkCanonical == 1)
return v.second;
bool samePath = (canonicalPath == QString::fromUtf8(filepath.c_str()));
FC_WARN("Identical physical path '" << canonicalPath.toUtf8().constData() << "'\n"
<< (samePath?"":" for file '") << (samePath?"":filepath.c_str()) << (samePath?"":"'\n")
<< " with existing document '" << v.second->Label.getValue()
<< "' in path: '" << v.second->FileName.getValue() << "'");
break;
}
}
return nullptr;
}
std::vector<Document*> Application::openDocuments(const std::vector<std::string> &filenames,
const std::vector<std::string> *paths,
const std::vector<std::string> *labels,
@@ -640,6 +681,7 @@ std::vector<Document*> Application::openDocuments(const std::vector<std::string>
_pendingDocs.clear();
_pendingDocsReopen.clear();
_pendingDocMap.clear();
_docReloadAttempts.clear();
signalStartOpenDocument();
@@ -649,118 +691,162 @@ std::vector<Document*> Application::openDocuments(const std::vector<std::string>
for (auto &name : filenames)
_pendingDocs.push_back(name.c_str());
std::map<Document *, DocTiming> newDocs;
std::map<DocumentT, DocTiming> timings;
FC_TIME_INIT(t);
for (std::size_t count=0;; ++count) {
const char *name = _pendingDocs.front();
_pendingDocs.pop_front();
bool isMainDoc = count < filenames.size();
std::vector<DocumentT> openedDocs;
try {
_objCount = -1;
std::set<std::string> objNames;
if (_allowPartial) {
auto it = _pendingDocMap.find(name);
if (it != _pendingDocMap.end())
objNames.swap(it->second);
}
int pass = 0;
do {
std::set<App::DocumentT> newDocs;
for (std::size_t count=0;; ++count) {
std::string name = std::move(_pendingDocs.front());
_pendingDocs.pop_front();
bool isMainDoc = (pass == 0 && count < filenames.size());
FC_TIME_INIT(t1);
DocTiming timing;
try {
_objCount = -1;
std::vector<std::string> objNames;
if (_allowPartial) {
auto it = _pendingDocMap.find(name);
if (it != _pendingDocMap.end()) {
if(isMainDoc)
it->second.clear();
else
objNames.swap(it->second);
_pendingDocMap.erase(it);
}
}
const char *path = name;
const char *label = 0;
if (isMainDoc) {
if (paths && paths->size()>count)
path = (*paths)[count].c_str();
FC_TIME_INIT(t1);
DocTiming timing;
if (labels && labels->size()>count)
label = (*labels)[count].c_str();
}
const char *path = name.c_str();
const char *label = 0;
if (isMainDoc) {
if (paths && paths->size()>count)
path = (*paths)[count].c_str();
auto doc = openDocumentPrivate(path, name, label, isMainDoc, createView, objNames);
FC_DURATION_PLUS(timing.d1,t1);
if (doc)
newDocs.emplace(doc,timing);
if (labels && labels->size()>count)
label = (*labels)[count].c_str();
}
auto doc = openDocumentPrivate(path, name.c_str(), label, isMainDoc, createView, std::move(objNames));
FC_DURATION_PLUS(timing.d1,t1);
if (doc) {
timings[doc].d1 += timing.d1;
newDocs.emplace(doc);
}
if (isMainDoc)
res[count] = doc;
_objCount = -1;
}
catch (const Base::Exception &e) {
if (!errs && isMainDoc)
throw;
if (errs && isMainDoc)
(*errs)[count] = e.what();
else
Console().Error("Exception opening file: %s [%s]\n", name, e.what());
}
catch (const std::exception &e) {
if (!errs && isMainDoc)
throw;
if (errs && isMainDoc)
(*errs)[count] = e.what();
else
Console().Error("Exception opening file: %s [%s]\n", name, e.what());
}
catch (...) {
if (errs) {
if (isMainDoc)
(*errs)[count] = "unknown error";
res[count] = doc;
_objCount = -1;
}
else {
_pendingDocs.clear();
catch (const Base::Exception &e) {
e.ReportException();
if (!errs && isMainDoc)
throw;
if (errs && isMainDoc)
(*errs)[count] = e.what();
else
Console().Error("Exception opening file: %s [%s]\n", name.c_str(), e.what());
}
catch (const std::exception &e) {
if (!errs && isMainDoc)
throw;
if (errs && isMainDoc)
(*errs)[count] = e.what();
else
Console().Error("Exception opening file: %s [%s]\n", name.c_str(), e.what());
}
catch (...) {
if (errs) {
if (isMainDoc)
(*errs)[count] = "unknown error";
}
else {
_pendingDocs.clear();
_pendingDocsReopen.clear();
_pendingDocMap.clear();
throw;
}
}
if (_pendingDocs.empty()) {
if(_pendingDocsReopen.empty())
break;
_pendingDocs = std::move(_pendingDocsReopen);
_pendingDocsReopen.clear();
_pendingDocMap.clear();
throw;
for(auto &file : _pendingDocs) {
auto doc = getDocumentByPath(file.c_str());
if(doc)
closeDocument(doc->getName());
}
}
}
if (_pendingDocs.empty()) {
if (_pendingDocsReopen.empty())
break;
_allowPartial = false;
_pendingDocs.swap(_pendingDocsReopen);
++pass;
_pendingDocMap.clear();
std::vector<Document*> docs;
docs.reserve(newDocs.size());
for(auto &d : newDocs) {
auto doc = d.getDocument();
if(!doc)
continue;
// Notify PropertyXLink to attach newly opened documents and restore
// relevant external links
PropertyXLink::restoreDocument(*doc);
docs.push_back(doc);
}
}
_pendingDocs.clear();
_pendingDocsReopen.clear();
_pendingDocMap.clear();
Base::SequencerLauncher seq("Postprocessing...", docs.size());
Base::SequencerLauncher seq("Postprocessing...", newDocs.size());
std::vector<Document*> docs;
docs.reserve(newDocs.size());
for (auto &v : newDocs) {
// Notify PropertyXLink to attach newly opened documents and restore
// relevant external links
PropertyXLink::restoreDocument(*v.first);
docs.push_back(v.first);
}
// After external links has been restored, we can now sort the document
// according to their dependency order.
docs = Document::getDependentDocuments(docs, true);
for (auto it=docs.begin(); it!=docs.end();) {
Document *doc = *it;
// It is possible that the newly opened document depends on an existing
// document, which will be included with the above call to
// Document::getDependentDocuments(). Make sure to exclude that.
auto dit = newDocs.find(doc);
if (dit == newDocs.end()) {
it = docs.erase(it);
continue;
// After external links has been restored, we can now sort the document
// according to their dependency order.
try {
docs = Document::getDependentDocuments(docs, true);
} catch (Base::Exception &e) {
e.ReportException();
}
++it;
FC_TIME_INIT(t1);
// Finalize document restoring with the correct order
doc->afterRestore(true);
FC_DURATION_PLUS(dit->second.d2,t1);
seq.next();
}
for(auto it=docs.begin(); it!=docs.end();) {
auto doc = *it;
// It is possible that the newly opened document depends on an existing
// document, which will be included with the above call to
// Document::getDependentDocuments(). Make sure to exclude that.
if(!newDocs.count(doc)) {
it = docs.erase(it);
continue;
}
auto &timing = timings[doc];
FC_TIME_INIT(t1);
// Finalize document restoring with the correct order
if(doc->afterRestore(true)) {
openedDocs.push_back(doc);
it = docs.erase(it);
} else {
++it;
// Here means this is a partial loaded document, and we need to
// reload it fully because of touched objects. The reason of
// reloading a partial document with touched object is because
// partial document is supposed to be readonly, while a
// 'touched' object requires recomputation. And an object may
// become touched during restoring if externally linked
// document time stamp mismatches with the stamp saved.
_pendingDocs.push_back(doc->FileName.getValue());
_pendingDocMap.erase(doc->FileName.getValue());
}
FC_DURATION_PLUS(timing.d2,t1);
seq.next();
}
// Close the document for reloading
for(auto doc : docs)
closeDocument(doc->getName());
}while(!_pendingDocs.empty());
// Set the active document using the first successfully restored main
// document (i.e. documents explicitly asked for by caller).
@@ -771,14 +857,14 @@ std::vector<Document*> Application::openDocuments(const std::vector<std::string>
}
}
for (auto doc : docs) {
auto &timing = newDocs[doc];
FC_DURATION_LOG(timing.d1, doc->getName() << " restore");
FC_DURATION_LOG(timing.d2, doc->getName() << " postprocess");
for (auto &doc : openedDocs) {
auto &timing = timings[doc];
FC_DURATION_LOG(timing.d1, doc.getDocumentName() << " restore");
FC_DURATION_LOG(timing.d2, doc.getDocumentName() << " postprocess");
}
FC_TIME_LOG(t,"total");
_isRestoring = false;
signalFinishOpenDocument();
return res;
}
@@ -786,7 +872,7 @@ std::vector<Document*> Application::openDocuments(const std::vector<std::string>
Document* Application::openDocumentPrivate(const char * FileName,
const char *propFileName, const char *label,
bool isMainDoc, bool createView,
const std::set<std::string> &objNames)
std::vector<std::string> &&objNames)
{
FileInfo File(FileName);
@@ -797,55 +883,51 @@ Document* Application::openDocumentPrivate(const char * FileName,
}
// Before creating a new document we check whether the document is already open
std::string filepath = File.filePath();
QString canonicalPath = QFileInfo(QString::fromUtf8(FileName)).canonicalFilePath();
for (std::map<std::string,Document*>::iterator it = DocMap.begin(); it != DocMap.end(); ++it) {
// get unique path separators
std::string fi = FileInfo(it->second->FileName.getValue()).filePath();
if (filepath != fi) {
if (canonicalPath == QFileInfo(QString::fromUtf8(fi.c_str())).canonicalFilePath()) {
bool samePath = (canonicalPath == QString::fromUtf8(FileName));
FC_WARN("Identical physical path '" << canonicalPath.toUtf8().constData() << "'\n"
<< (samePath?"":" for file '") << (samePath?"":FileName) << (samePath?"":"'\n")
<< " with existing document '" << it->second->Label.getValue()
<< "' in path: '" << it->second->FileName.getValue() << "'");
}
continue;
}
if(it->second->testStatus(App::Document::PartialDoc)
|| it->second->testStatus(App::Document::PartialRestore)) {
auto doc = getDocumentByPath(File.filePath().c_str(), 2);
if(doc) {
if(doc->testStatus(App::Document::PartialDoc)
|| doc->testStatus(App::Document::PartialRestore)) {
// Here means a document is already partially loaded, but the document
// is requested again, either partial or not. We must check if the
// document contains the required object
if(isMainDoc) {
// Main document must be open fully, so close and reopen
closeDocument(it->first.c_str());
break;
}
if(_allowPartial) {
closeDocument(doc->getName());
doc = nullptr;
} else if(_allowPartial) {
bool reopen = false;
for(auto &name : objNames) {
auto obj = it->second->getObject(name.c_str());
auto obj = doc->getObject(name.c_str());
if(!obj || obj->testStatus(App::PartialObject)) {
reopen = true;
// NOTE: We are about to reload this document with
// extra objects. However, it is possible to repeat
// this process several times, if it is linked by
// multiple documents and each with a different set of
// objects. To partially solve this problem, we do not
// close and reopen the document immediately here, but
// add it to _pendingDocsReopen to delay reloading.
for(auto obj : doc->getObjects())
objNames.push_back(obj->getNameInDocument());
_pendingDocMap[doc->FileName.getValue()] = std::move(objNames);
break;
}
}
if(!reopen)
return 0;
}
auto &names = _pendingDocMap[FileName];
names.clear();
_pendingDocsReopen.push_back(FileName);
return 0;
if(doc) {
_pendingDocsReopen.emplace_back(FileName);
return 0;
}
}
if(!isMainDoc)
return 0;
return it->second;
else if(doc)
return doc;
}
std::string name;
@@ -867,6 +949,8 @@ Document* Application::openDocumentPrivate(const char * FileName,
try {
// read the document
newDoc->restore(File.filePath().c_str(),true,objNames);
if(DocFileMap.size())
DocFileMap[FileInfo(newDoc->FileName.getValue()).filePath()] = newDoc;
return newDoc;
}
// if the project file itself is corrupt then
@@ -1463,6 +1547,7 @@ void Application::slotStartSaveDocument(const App::Document& doc, const std::str
void Application::slotFinishSaveDocument(const App::Document& doc, const std::string& filename)
{
DocFileMap.clear();
this->signalFinishSaveDocument(doc, filename);
}