diff --git a/kdevplatform/language/duchain/duchain.cpp b/kdevplatform/language/duchain/duchain.cpp index 913eee8559..240e3390f5 100644 --- a/kdevplatform/language/duchain/duchain.cpp +++ b/kdevplatform/language/duchain/duchain.cpp @@ -1,1746 +1,1746 @@ /* This is part of KDevelop Copyright 2006-2008 Hamish Rodda Copyright 2007-2008 David Nolden This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "duchain.h" #include "duchainlock.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "../interfaces/ilanguagesupport.h" #include "../interfaces/icodehighlighting.h" #include "../backgroundparser/backgroundparser.h" #include #include "language-features.h" #include "topducontext.h" #include "topducontextdata.h" #include "topducontextdynamicdata.h" #include "parsingenvironment.h" #include "declaration.h" #include "definitions.h" #include "duchainutils.h" #include "use.h" #include "uses.h" #include "abstractfunctiondeclaration.h" #include "duchainregister.h" #include "persistentsymboltable.h" #include "serialization/itemrepository.h" #include "waitforupdate.h" #include "importers.h" #if HAVE_MALLOC_TRIM #include "malloc.h" #endif namespace { //Additional "soft" cleanup steps that are done before the actual cleanup. //During "soft" cleanup, the consistency is not guaranteed. The repository is //marked to be updating during soft cleanup, so if kdevelop crashes, it will be cleared. //The big advantage of the soft cleanup steps is, that the duchain is always only locked for //short times, which leads to no lockup in the UI. const int SOFT_CLEANUP_STEPS = 1; // seconds to wait before trying to cleanup the DUChain const uint cleanupEverySeconds = 200; ///Approximate maximum count of top-contexts that are checked during final cleanup const uint maxFinalCleanupCheckContexts = 2000; const uint minimumFinalCleanupCheckContextsPercentage = 10; //Check at least n% of all top-contexts during cleanup //Set to true as soon as the duchain is deleted } namespace KDevelop { bool DUChain::m_deleted = false; ///Must be locked through KDevelop::SpinLock before using chainsByIndex ///This lock should be locked only for very short times QMutex DUChain::chainsByIndexLock; std::vector DUChain::chainsByIndex; //This thing is not actually used, but it's needed for compiling DEFINE_LIST_MEMBER_HASH(EnvironmentInformationListItem, items, uint) //An entry for the item-repository that holds some meta-data. Behind this entry, the actual ParsingEnvironmentFileData is stored. class EnvironmentInformationItem { public: EnvironmentInformationItem(uint topContext, uint size) : m_topContext(topContext), m_size(size) { } ~EnvironmentInformationItem() { } unsigned int hash() const { return m_topContext; } unsigned int itemSize() const { return sizeof(*this) + m_size; } uint m_topContext; uint m_size;//Size of the data behind, that holds the actual item }; struct ItemRepositoryIndexHash { uint operator()(unsigned int __x) const { return 173*(__x>>2) + 11 * (__x >> 16); } }; class EnvironmentInformationRequest { public: ///This constructor should only be used for lookup EnvironmentInformationRequest(uint topContextIndex) : m_file(nullptr), m_index(topContextIndex) { } EnvironmentInformationRequest(const ParsingEnvironmentFile* file) : m_file(file), m_index(file->indexedTopContext().index()) { } enum { AverageSize = 32 //This should be the approximate average size of an Item }; unsigned int hash() const { return m_index; } uint itemSize() const { return sizeof(EnvironmentInformationItem) + DUChainItemSystem::self().dynamicSize(*m_file->d_func()); } void createItem(EnvironmentInformationItem* item) const { new (item) EnvironmentInformationItem(m_index, DUChainItemSystem::self().dynamicSize(*m_file->d_func())); Q_ASSERT(m_file->d_func()->m_dynamic); DUChainBaseData* data = reinterpret_cast(reinterpret_cast(item) + sizeof(EnvironmentInformationItem)); DUChainItemSystem::self().copy(*m_file->d_func(), *data, true); Q_ASSERT(data->m_range == m_file->d_func()->m_range); Q_ASSERT(data->classId == m_file->d_func()->classId); Q_ASSERT(data->m_dynamic == false); } static void destroy(EnvironmentInformationItem* item, KDevelop::AbstractItemRepository&) { item->~EnvironmentInformationItem(); //We don't need to call the destructor, because that's done in DUChainBase::makeDynamic() //We just need to make sure that every environment-file is dynamic when it's deleted // DUChainItemSystem::self().callDestructor((DUChainBaseData*)(((char*)item) + sizeof(EnvironmentInformationItem))); } static bool persistent(const EnvironmentInformationItem* ) { //Cleanup done separately return true; } bool equals(const EnvironmentInformationItem* item) const { return m_index == item->m_topContext; } const ParsingEnvironmentFile* m_file; uint m_index; }; ///A list of environment-information/top-contexts mapped to a file-name class EnvironmentInformationListItem { public: EnvironmentInformationListItem() { initializeAppendedLists(true); } EnvironmentInformationListItem(const EnvironmentInformationListItem& rhs, bool dynamic = true) { initializeAppendedLists(dynamic); m_file = rhs.m_file; copyListsFrom(rhs); } ~EnvironmentInformationListItem() { freeAppendedLists(); } unsigned int hash() const { //We only compare the declaration. This allows us implementing a map, although the item-repository //originally represents a set. return m_file.hash(); } unsigned short int itemSize() const { return dynamicSize(); } IndexedString m_file; uint classSize() const { return sizeof(*this); } START_APPENDED_LISTS(EnvironmentInformationListItem); ///Contains the index of each contained environment-item APPENDED_LIST_FIRST(EnvironmentInformationListItem, uint, items); END_APPENDED_LISTS(EnvironmentInformationListItem, items); }; class EnvironmentInformationListRequest { public: ///This constructor should only be used for lookup EnvironmentInformationListRequest(const IndexedString& file) : m_file(file), m_item(nullptr) { } ///This is used to actually construct the information in the repository EnvironmentInformationListRequest(const IndexedString& file, const EnvironmentInformationListItem& item) : m_file(file), m_item(&item) { } enum { AverageSize = 160 //This should be the approximate average size of an Item }; unsigned int hash() const { return m_file.hash(); } uint itemSize() const { return m_item->itemSize(); } void createItem(EnvironmentInformationListItem* item) const { Q_ASSERT(m_item->m_file == m_file); new (item) EnvironmentInformationListItem(*m_item, false); } static void destroy(EnvironmentInformationListItem* item, KDevelop::AbstractItemRepository&) { item->~EnvironmentInformationListItem(); } static bool persistent(const EnvironmentInformationListItem*) { //Cleanup is done separately return true; } bool equals(const EnvironmentInformationListItem* item) const { return m_file == item->m_file; } IndexedString m_file; const EnvironmentInformationListItem* m_item; }; class DUChainPrivate; static DUChainPrivate* duChainPrivateSelf = nullptr; class DUChainPrivate { class CleanupThread : public QThread { public: explicit CleanupThread(DUChainPrivate* data) : m_data(data) { } void stopThread() { quit(); wait(); } private: void run() override { QTimer timer; - connect(&timer, &QTimer::timeout, [this]() { + connect(&timer, &QTimer::timeout, this, [this]() { //Just to make sure the cache is cleared periodically ModificationRevisionSet::clearCache(); m_data->doMoreCleanup(SOFT_CLEANUP_STEPS, TryLock); }); timer.start(cleanupEverySeconds * 1000); exec(); } DUChainPrivate* m_data; }; public: DUChainPrivate() : m_chainsMutex(QMutex::Recursive), m_cleanupMutex(QMutex::Recursive), instance(nullptr), m_cleanupDisabled(false), m_destroyed(false), m_environmentListInfo(QStringLiteral("Environment Lists")), m_environmentInfo(QStringLiteral("Environment Information")) { #if defined(TEST_NO_CLEANUP) m_cleanupDisabled = true; #endif duChainPrivateSelf = this; qRegisterMetaType("KDevelop::DUChainBasePointer"); qRegisterMetaType("KDevelop::DUContextPointer"); qRegisterMetaType("KDevelop::TopDUContextPointer"); qRegisterMetaType("KDevelop::DeclarationPointer"); qRegisterMetaType("KDevelop::FunctionDeclarationPointer"); qRegisterMetaType("KDevelop::IndexedString"); qRegisterMetaType("KDevelop::IndexedTopDUContext"); qRegisterMetaType("KDevelop::ReferencedTopDUContext"); instance = new DUChain(); m_cleanup = new CleanupThread(this); m_cleanup->start(); DUChain::m_deleted = false; ///Loading of some static data: { ///@todo Solve this more duchain-like QFile f(globalItemRepositoryRegistry().path() + QLatin1String("/parsing_environment_data")); bool opened = f.open(QIODevice::ReadOnly); ///FIXME: ugh, so ugly ParsingEnvironmentFile::m_staticData = reinterpret_cast( new char[sizeof(StaticParsingEnvironmentData)]); if(opened) { qCDebug(LANGUAGE) << "reading parsing-environment static data"; //Read f.read((char*)ParsingEnvironmentFile::m_staticData, sizeof(StaticParsingEnvironmentData)); }else{ qCDebug(LANGUAGE) << "creating new parsing-environment static data"; //Initialize new (ParsingEnvironmentFile::m_staticData) StaticParsingEnvironmentData(); } } ///Read in the list of available top-context indices { QFile f(globalItemRepositoryRegistry().path() + QLatin1String("/available_top_context_indices")); bool opened = f.open(QIODevice::ReadOnly); if(opened) { Q_ASSERT( (f.size() % sizeof(uint)) == 0); m_availableTopContextIndices.resize(f.size()/(int)sizeof(uint)); f.read((char*)m_availableTopContextIndices.data(), f.size()); } } } ~DUChainPrivate() { qCDebug(LANGUAGE) << "Destroying"; DUChain::m_deleted = true; m_cleanup->stopThread(); delete m_cleanup; delete instance; } void clear() { if(!m_cleanupDisabled) doMoreCleanup(); DUChainWriteLocker writeLock(DUChain::lock()); QMutexLocker l(&m_chainsMutex); foreach(TopDUContext* top, m_chainsByUrl) removeDocumentChainFromMemory(top); m_indexEnvironmentInformations.clear(); m_fileEnvironmentInformations.clear(); Q_ASSERT(m_fileEnvironmentInformations.isEmpty()); Q_ASSERT(m_chainsByUrl.isEmpty()); } ///DUChain must be write-locked ///Also removes from the environment-manager if the top-context is not on disk void removeDocumentChainFromMemory(TopDUContext* context) { QMutexLocker l(&m_chainsMutex); { QMutexLocker l(&m_referenceCountsMutex); if(m_referenceCounts.contains(context)) { //This happens during shutdown, since everything is unloaded qCDebug(LANGUAGE) << "removed a top-context that was reference-counted:" << context->url().str() << context->ownIndex(); m_referenceCounts.remove(context); } } uint index = context->ownIndex(); // qCDebug(LANGUAGE) << "duchain: removing document" << context->url().str(); Q_ASSERT(hasChainForIndex(index)); Q_ASSERT(m_chainsByUrl.contains(context->url(), context)); m_chainsByUrl.remove(context->url(), context); if(!context->isOnDisk()) instance->removeFromEnvironmentManager(context); l.unlock(); //DUChain is write-locked, so we can do whatever we want on the top-context, including deleting it context->deleteSelf(); l.relock(); Q_ASSERT(hasChainForIndex(index)); QMutexLocker lock(&DUChain::chainsByIndexLock); DUChain::chainsByIndex[index] = nullptr; } ///Must be locked before accessing content of this class. ///Should be released during expensive disk-operations and such. QMutex m_chainsMutex; QMutex m_cleanupMutex; CleanupThread* m_cleanup; DUChain* instance; DUChainLock lock; QMultiMap m_chainsByUrl; //Must be locked before accessing m_referenceCounts QMutex m_referenceCountsMutex; QHash m_referenceCounts; Definitions m_definitions; Uses m_uses; QSet m_loading; bool m_cleanupDisabled; //List of available top-context indices, protected by m_chainsMutex QVector m_availableTopContextIndices; ///Used to keep alive the top-context that belong to documents loaded in the editor QSet m_openDocumentContexts; bool m_destroyed; ///The item must not be stored yet ///m_chainsMutex should not be locked, since this can trigger I/O void addEnvironmentInformation(ParsingEnvironmentFilePointer info) { Q_ASSERT(!findInformation(info->indexedTopContext().index())); Q_ASSERT(m_environmentInfo.findIndex(info->indexedTopContext().index()) == 0); QMutexLocker lock(&m_chainsMutex); m_fileEnvironmentInformations.insert(info->url(), info); m_indexEnvironmentInformations.insert(info->indexedTopContext().index(), info); Q_ASSERT(info->d_func()->classId); } ///The item must be managed currently ///m_chainsMutex does not need to be locked void removeEnvironmentInformation(ParsingEnvironmentFilePointer info) { info->makeDynamic(); //By doing this, we make sure the data is actually being destroyed in the destructor bool removed = false; bool removed2 = false; { QMutexLocker lock(&m_chainsMutex); removed = m_fileEnvironmentInformations.remove(info->url(), info); removed2 = m_indexEnvironmentInformations.remove(info->indexedTopContext().index()); } { //Remove it from the environment information lists if it was there QMutexLocker lock(m_environmentListInfo.mutex()); uint index = m_environmentListInfo.findIndex(info->url()); if(index) { EnvironmentInformationListItem item(*m_environmentListInfo.itemFromIndex(index)); if(item.itemsList().removeOne(info->indexedTopContext().index())) { m_environmentListInfo.deleteItem(index); if(!item.itemsList().isEmpty()) m_environmentListInfo.index(EnvironmentInformationListRequest(info->url(), item)); } } } QMutexLocker lock(m_environmentInfo.mutex()); uint index = m_environmentInfo.findIndex(info->indexedTopContext().index()); if(index) { m_environmentInfo.deleteItem(index); } Q_UNUSED(removed); Q_UNUSED(removed2); Q_ASSERT(index || (removed && removed2)); Q_ASSERT(!findInformation(info->indexedTopContext().index())); } ///m_chainsMutex should _not_ be locked, because this may trigger I/O QList getEnvironmentInformation(const IndexedString& url) { QList ret; uint listIndex = m_environmentListInfo.findIndex(url); if(listIndex) { KDevVarLengthArray topContextIndices; { //First store all the possible intices into the KDevVarLengthArray, so we can unlock the mutex before processing them. QMutexLocker lock(m_environmentListInfo.mutex()); //Lock the mutex to make sure the item isn't changed while it's being iterated const EnvironmentInformationListItem* item = m_environmentListInfo.itemFromIndex(listIndex); FOREACH_FUNCTION(uint topContextIndex, item->items) topContextIndices << topContextIndex; } //Process the indices in a separate step after copying them from the array, so we don't need m_environmentListInfo.mutex locked, //and can call loadInformation(..) safely, which else might lead to a deadlock. foreach (uint topContextIndex, topContextIndices) { QExplicitlySharedDataPointer< ParsingEnvironmentFile > p = ParsingEnvironmentFilePointer(loadInformation(topContextIndex)); if(p) { ret << p; }else{ qCDebug(LANGUAGE) << "Failed to load enviromment-information for" << TopDUContextDynamicData::loadUrl(topContextIndex).str(); } } } QMutexLocker l(&m_chainsMutex); //Add those information that have not been added to the stored lists yet foreach(const ParsingEnvironmentFilePointer& file, m_fileEnvironmentInformations.values(url)) if(!ret.contains(file)) ret << file; return ret; } ///Must be called _without_ the chainsByIndex spin-lock locked static inline bool hasChainForIndex(uint index) { QMutexLocker lock(&DUChain::chainsByIndexLock); return (DUChain::chainsByIndex.size() > index) && DUChain::chainsByIndex[index]; } ///Must be called _without_ the chainsByIndex spin-lock locked. Returns the top-context if it is loaded. static inline TopDUContext* readChainForIndex(uint index) { QMutexLocker lock(&DUChain::chainsByIndexLock); if(DUChain::chainsByIndex.size() > index) return DUChain::chainsByIndex[index]; else return nullptr; } ///Makes sure that the chain with the given index is loaded ///@warning m_chainsMutex must NOT be locked when this is called void loadChain(uint index, QSet& loaded) { QMutexLocker l(&m_chainsMutex); if(!hasChainForIndex(index)) { if(m_loading.contains(index)) { //It's probably being loaded by another thread. So wait until the load is ready while(m_loading.contains(index)) { l.unlock(); qCDebug(LANGUAGE) << "waiting for another thread to load index" << index; QThread::usleep(50000); l.relock(); } loaded.insert(index); return; } m_loading.insert(index); loaded.insert(index); l.unlock(); qCDebug(LANGUAGE) << "loading top-context" << index; TopDUContext* chain = TopDUContextDynamicData::load(index); if(chain) { chain->setParsingEnvironmentFile(loadInformation(chain->ownIndex())); if(!chain->usingImportsCache()) { //Eventually also load all the imported chains, so the import-structure is built foreach(const DUContext::Import &import, chain->DUContext::importedParentContexts()) { if(!loaded.contains(import.topContextIndex())) { loadChain(import.topContextIndex(), loaded); } } } chain->rebuildDynamicImportStructure(); chain->setInDuChain(true); instance->addDocumentChain(chain); } l.relock(); m_loading.remove(index); } } ///Stores all environment-information ///Also makes sure that all information that stays is referenced, so it stays alive. ///@param atomic If this is false, the write-lock will be released time by time void storeAllInformation(bool atomic, DUChainWriteLocker& locker) { uint cnt = 0; QList urls; { QMutexLocker lock(&m_chainsMutex); urls += m_fileEnvironmentInformations.keys(); } foreach(const IndexedString &url, urls) { QList check; { QMutexLocker lock(&m_chainsMutex); check = m_fileEnvironmentInformations.values(url); } foreach(ParsingEnvironmentFilePointer file, check) { EnvironmentInformationRequest req(file.data()); QMutexLocker lock(m_environmentInfo.mutex()); uint index = m_environmentInfo.findIndex(req); if(file->d_func()->isDynamic()) { //This item has been changed, or isn't in the repository yet //Eventually remove an old entry if(index) m_environmentInfo.deleteItem(index); //Add the new entry to the item repository index = m_environmentInfo.index(req); Q_ASSERT(index); EnvironmentInformationItem* item = const_cast(m_environmentInfo.itemFromIndex(index)); DUChainBaseData* theData = reinterpret_cast(reinterpret_cast(item) + sizeof(EnvironmentInformationItem)); Q_ASSERT(theData->m_range == file->d_func()->m_range); Q_ASSERT(theData->m_dynamic == false); Q_ASSERT(theData->classId == file->d_func()->classId); file->setData( theData ); ++cnt; }else{ m_environmentInfo.itemFromIndex(index); //Prevent unloading of the data, by accessing the item } } ///We must not release the lock while holding a reference to a ParsingEnvironmentFilePointer, else we may miss the deletion of an ///information, and will get crashes. if(!atomic && (cnt % 100 == 0)) { //Release the lock on a regular basis locker.unlock(); locker.lock(); } storeInformationList(url); //Access the data in the repository, so the bucket isn't unloaded uint index = m_environmentListInfo.findIndex(EnvironmentInformationListRequest(url)); if(index) { m_environmentListInfo.itemFromIndex(index); }else{ QMutexLocker lock(&m_chainsMutex); qCDebug(LANGUAGE) << "Did not find stored item for" << url.str() << "count:" << m_fileEnvironmentInformations.values(url); } if(!atomic) { locker.unlock(); locker.lock(); } } } QMutex& cleanupMutex() { return m_cleanupMutex; } /// defines how we interact with the ongoing language parse jobs enum LockFlag { /// no locking required, only used when we locked previously NoLock = 0, /// lock all parse jobs and block until we succeeded. required at shutdown BlockingLock = 1, /// only try to lock and abort on failure, good for the intermittent cleanups TryLock = 2, }; ///@param retries When this is nonzero, then doMoreCleanup will do the specified amount of cycles ///doing the cleanup without permanently locking the du-chain. During these steps the consistency ///of the disk-storage is not guaranteed, but only few changes will be done during these steps, ///so the final step where the duchain is permanently locked is much faster. void doMoreCleanup(int retries = 0, LockFlag lockFlag = BlockingLock) { if(m_cleanupDisabled) return; //This mutex makes sure that there's never 2 threads at he same time trying to clean up QMutexLocker lockCleanupMutex(&cleanupMutex()); if(m_destroyed || m_cleanupDisabled) return; Q_ASSERT(!instance->lock()->currentThreadHasReadLock() && !instance->lock()->currentThreadHasWriteLock()); DUChainWriteLocker writeLock(instance->lock()); //This is used to stop all parsing before starting to do the cleanup. This way less happens during the //soft cleanups, and we have a good chance that during the "hard" cleanup only few data has to be written. QList locked; if (lockFlag != NoLock) { QList languages; if (ICore* core = ICore::self()) if (ILanguageController* lc = core->languageController()) languages = lc->loadedLanguages(); writeLock.unlock(); //Here we wait for all parsing-threads to stop their processing foreach(const auto language, languages) { if (lockFlag == TryLock) { if (!language->parseLock()->tryLockForWrite()) { qCDebug(LANGUAGE) << "Aborting cleanup because language plugin is still parsing:" << language->name(); // some language is still parsing, don't interfere with the cleanup foreach(auto* lock, locked) { lock->unlock(); } return; } } else { language->parseLock()->lockForWrite(); } locked << language->parseLock(); } writeLock.lock(); globalItemRepositoryRegistry().lockForWriting(); qCDebug(LANGUAGE) << "starting cleanup"; } QTime startTime = QTime::currentTime(); PersistentSymbolTable::self().clearCache(); storeAllInformation(!retries, writeLock); //Puts environment-information into a repository //We don't need to increase the reference-count, since the cleanup-mutex is locked QSet workOnContexts; { QMutexLocker l(&m_chainsMutex); workOnContexts.reserve(m_chainsByUrl.size()); foreach(TopDUContext* top, m_chainsByUrl) { workOnContexts << top; Q_ASSERT(hasChainForIndex(top->ownIndex())); } } foreach(TopDUContext* context, workOnContexts) { context->m_dynamicData->store(); if(retries) { //Eventually give other threads a chance to access the duchain writeLock.unlock(); //Sleep to give the other threads a realistic chance to get a read-lock in between QThread::usleep(500); writeLock.lock(); } } //Unload all top-contexts that don't have a reference-count and that are not imported by a referenced one QSet unloadedNames; bool unloadedOne = true; bool unloadAllUnreferenced = !retries; //Now unload contexts, but only ones that are not imported by any other currently loaded context //The complication: Since during the lock-break new references may be added, we must never keep //the du-chain in an invalid state. Thus we can only unload contexts that are not imported by any //currently loaded contexts. In case of loops, we have to unload everything at once. while(unloadedOne) { unloadedOne = false; int hadUnloadable = 0; unloadContexts: foreach(TopDUContext* unload, workOnContexts) { bool hasReference = false; { QMutexLocker l(&m_referenceCountsMutex); //Test if the context is imported by a referenced one for (auto it = m_referenceCounts.constBegin(), end = m_referenceCounts.constEnd(); it != end; ++it) { auto* context = it.key(); if(context == unload || context->imports(unload, CursorInRevision())) { workOnContexts.remove(unload); hasReference = true; } } } if(!hasReference) ++hadUnloadable; //We have found a context that is not referenced else continue; //This context is referenced bool isImportedByLoaded = !unload->loadedImporters().isEmpty(); //If we unload a context that is imported by other contexts, we create a bad loaded state if(isImportedByLoaded && !unloadAllUnreferenced) continue; unloadedNames.insert(unload->url()); //Since we've released the write-lock in between, we've got to call store() again to be sure that none of the data is dynamic //If nothing has changed, it is only a low-cost call. unload->m_dynamicData->store(); Q_ASSERT(!unload->d_func()->m_dynamic); removeDocumentChainFromMemory(unload); workOnContexts.remove(unload); unloadedOne = true; if(!unloadAllUnreferenced) { //Eventually give other threads a chance to access the duchain writeLock.unlock(); //Sleep to give the other threads a realistic chance to get a read-lock in between QThread::usleep(500); writeLock.lock(); } } if(hadUnloadable && !unloadedOne) { Q_ASSERT(!unloadAllUnreferenced); //This can happen in case of loops. We have o unload everything at one time. qCDebug(LANGUAGE) << "found" << hadUnloadable << "unloadable contexts, but could not unload separately. Unloading atomically."; unloadAllUnreferenced = true; hadUnloadable = 0; //Reset to 0, so we cannot loop forever goto unloadContexts; } } if(retries == 0) { QMutexLocker lock(&m_chainsMutex); //Do this atomically, since we must be sure that _everything_ is already saved for(QMultiMap::iterator it = m_fileEnvironmentInformations.begin(); it != m_fileEnvironmentInformations.end(); ) { ParsingEnvironmentFile* f = it->data(); Q_ASSERT(f->d_func()->classId); if(f->ref.load() == 1) { Q_ASSERT(!f->d_func()->isDynamic()); //It cannot be dynamic, since we have stored before //The ParsingEnvironmentFilePointer is only referenced once. This means that it does not belong to any //loaded top-context, so just remove it to save some memory and processing time. ///@todo use some kind of timeout before removing it = m_fileEnvironmentInformations.erase(it); }else{ ++it; } } } if(retries) writeLock.unlock(); //This must be the last step, due to the on-disk reference counting globalItemRepositoryRegistry().store(); //Stores all repositories { //Store the static parsing-environment file data ///@todo Solve this more elegantly, using a general mechanism to store static duchain-like data Q_ASSERT(ParsingEnvironmentFile::m_staticData); QFile f(globalItemRepositoryRegistry().path() + QLatin1String("/parsing_environment_data")); bool opened = f.open(QIODevice::WriteOnly); Q_ASSERT(opened); Q_UNUSED(opened); f.write((char*)ParsingEnvironmentFile::m_staticData, sizeof(StaticParsingEnvironmentData)); } ///Write out the list of available top-context indices { QMutexLocker lock(&m_chainsMutex); QFile f(globalItemRepositoryRegistry().path() + QLatin1String("/available_top_context_indices")); bool opened = f.open(QIODevice::WriteOnly); Q_ASSERT(opened); Q_UNUSED(opened); f.write((char*)m_availableTopContextIndices.data(), m_availableTopContextIndices.size() * sizeof(uint)); } if(retries) { doMoreCleanup(retries-1, NoLock); writeLock.lock(); } if(lockFlag != NoLock) { globalItemRepositoryRegistry().unlockForWriting(); const auto elapsedMS = startTime.msecsTo(QTime::currentTime()); qCDebug(LANGUAGE) << "time spent doing cleanup:" << elapsedMS << "ms - top-contexts still open:" << m_chainsByUrl.size() << "- retries" << retries; } foreach(QReadWriteLock* lock, locked) lock->unlock(); #if HAVE_MALLOC_TRIM // trim unused memory but keep a pad buffer of about 50 MB // this can greatly decrease the perceived memory consumption of kdevelop // see: https://sourceware.org/bugzilla/show_bug.cgi?id=14827 malloc_trim(50 * 1024 * 1024); #endif } ///Checks whether the information is already loaded. ParsingEnvironmentFile* findInformation(uint topContextIndex) { QMutexLocker lock(&m_chainsMutex); QHash::iterator it = m_indexEnvironmentInformations.find(topContextIndex); if(it != m_indexEnvironmentInformations.end()) return (*it).data(); return nullptr; } ///Loads/gets the environment-information for the given top-context index, or returns zero if none exists ///@warning m_chainsMutex should NOT be locked when this is called, because it triggers I/O ///@warning no other mutexes should be locked, as that may lead to a dedalock ParsingEnvironmentFile* loadInformation(uint topContextIndex) { ParsingEnvironmentFile* alreadyLoaded = findInformation(topContextIndex); if(alreadyLoaded) return alreadyLoaded; //Step two: Check if it is on disk, and if is, load it uint dataIndex = m_environmentInfo.findIndex(EnvironmentInformationRequest(topContextIndex)); if(!dataIndex) { //No environment-information stored for this top-context return nullptr; } const EnvironmentInformationItem& item(*m_environmentInfo.itemFromIndex(dataIndex)); QMutexLocker lock(&m_chainsMutex); //Due to multi-threading, we must do this check after locking the mutex, so we can be sure we don't create the same item twice at the same time alreadyLoaded = findInformation(topContextIndex); if(alreadyLoaded) return alreadyLoaded; ///FIXME: ugly, and remove const_cast ParsingEnvironmentFile* ret = dynamic_cast(DUChainItemSystem::self().create( const_cast(reinterpret_cast(reinterpret_cast(&item) + sizeof(EnvironmentInformationItem))) )); if(ret) { Q_ASSERT(ret->d_func()->classId); Q_ASSERT(ret->indexedTopContext().index() == topContextIndex); ParsingEnvironmentFilePointer retPtr(ret); m_fileEnvironmentInformations.insert(ret->url(), retPtr); Q_ASSERT(!m_indexEnvironmentInformations.contains(ret->indexedTopContext().index())); m_indexEnvironmentInformations.insert(ret->indexedTopContext().index(), retPtr); } return ret; } struct CleanupListVisitor { QList checkContexts; bool operator()(const EnvironmentInformationItem* item) { checkContexts << item->m_topContext; return true; } }; ///Will check a selection of all top-contexts for up-to-date ness, and remove them if out of date void cleanupTopContexts() { DUChainWriteLocker lock( DUChain::lock() ); qCDebug(LANGUAGE) << "cleaning top-contexts"; CleanupListVisitor visitor; uint startPos = 0; m_environmentInfo.visitAllItems(visitor); int checkContextsCount = maxFinalCleanupCheckContexts; int percentageOfContexts = (visitor.checkContexts.size() * 100) / minimumFinalCleanupCheckContextsPercentage; if(checkContextsCount < percentageOfContexts) checkContextsCount = percentageOfContexts; if(visitor.checkContexts.size() > (int)checkContextsCount) startPos = qrand() % (visitor.checkContexts.size() - checkContextsCount); int endPos = startPos + maxFinalCleanupCheckContexts; if(endPos > visitor.checkContexts.size()) endPos = visitor.checkContexts.size(); QSet< uint > check; for(int a = startPos; a < endPos && check.size() < checkContextsCount; ++a) if(check.size() < checkContextsCount) addContextsForRemoval(check, IndexedTopDUContext(visitor.checkContexts[a])); foreach(uint topIndex, check) { IndexedTopDUContext top(topIndex); if(top.data()) { qCDebug(LANGUAGE) << "removing top-context for" << top.data()->url().str() << "because it is out of date"; instance->removeDocumentChain(top.data()); } } qCDebug(LANGUAGE) << "check ready"; } private: void addContextsForRemoval(QSet& topContexts, IndexedTopDUContext top) { if(topContexts.contains(top.index())) return; QExplicitlySharedDataPointer info( instance->environmentFileForDocument(top) ); ///@todo Also check if the context is "useful"(Not a duplicate context, imported by a useful one, ...) if(info && info->needsUpdate()) { //This context will be removed }else{ return; } topContexts.insert(top.index()); if(info) { //Check whether importers need to be removed as well QList< QExplicitlySharedDataPointer > importers = info->importers(); QSet< QExplicitlySharedDataPointer > checkNext; //Do breadth first search, so less imports/importers have to be loaded, and a lower depth is reached for(QList< QExplicitlySharedDataPointer >::iterator it = importers.begin(); it != importers.end(); ++it) { IndexedTopDUContext c = (*it)->indexedTopContext(); if(!topContexts.contains(c.index())) { topContexts.insert(c.index()); //Prevent useless recursion checkNext.insert(*it); } } for(QSet< QExplicitlySharedDataPointer >::const_iterator it = checkNext.begin(); it != checkNext.end(); ++it) { topContexts.remove((*it)->indexedTopContext().index()); //Enable full check again addContextsForRemoval(topContexts, (*it)->indexedTopContext()); } } } ///Stores the environment-information for the given url void storeInformationList(const IndexedString& url) { QMutexLocker lock(m_environmentListInfo.mutex()); EnvironmentInformationListItem newItem; newItem.m_file = url; QSet newItems; { QMutexLocker lock(&m_chainsMutex); QMultiMap::iterator start = m_fileEnvironmentInformations.lowerBound(url); QMultiMap::iterator end = m_fileEnvironmentInformations.upperBound(url); for(QMultiMap::iterator it = start; it != end; ++it) { uint topContextIndex = (*it)->indexedTopContext().index(); newItems.insert(topContextIndex); newItem.itemsList().append(topContextIndex); } } uint index = m_environmentListInfo.findIndex(url); if(index) { //We only handle adding items here, since we can never be sure whether everything is loaded //Removal is handled directly in removeEnvironmentInformation const EnvironmentInformationListItem* item = m_environmentListInfo.itemFromIndex(index); QSet oldItems; FOREACH_FUNCTION(uint topContextIndex, item->items) { oldItems.insert(topContextIndex); if(!newItems.contains(topContextIndex)) { newItems.insert(topContextIndex); newItem.itemsList().append(topContextIndex); } } if(oldItems == newItems) return; ///Update/insert a new list m_environmentListInfo.deleteItem(index); //Remove the previous item } Q_ASSERT(m_environmentListInfo.findIndex(EnvironmentInformationListRequest(url)) == 0); //Insert the new item m_environmentListInfo.index(EnvironmentInformationListRequest(url, newItem)); Q_ASSERT(m_environmentListInfo.findIndex(EnvironmentInformationListRequest(url))); } //Loaded environment information. Protected by m_chainsMutex QMultiMap m_fileEnvironmentInformations; QHash m_indexEnvironmentInformations; ///The following repositories are thread-safe, and m_chainsMutex should not be locked when using them, because ///they may trigger I/O. Still it may be required to lock their local mutexes. ///Maps filenames to a list of top-contexts/environment-information. ItemRepository m_environmentListInfo; ///Maps top-context-indices to environment-information item. ItemRepository m_environmentInfo; }; Q_GLOBAL_STATIC(DUChainPrivate, sdDUChainPrivate) DUChain::DUChain() { Q_ASSERT(ICore::self()); connect(ICore::self()->documentController(), &IDocumentController::documentLoadedPrepare, this, &DUChain::documentLoadedPrepare); connect(ICore::self()->documentController(), &IDocumentController::documentUrlChanged, this, &DUChain::documentRenamed); connect(ICore::self()->documentController(), &IDocumentController::documentActivated, this, &DUChain::documentActivated); connect(ICore::self()->documentController(), &IDocumentController::documentClosed, this, &DUChain::documentClosed); } DUChain::~DUChain() { DUChain::m_deleted = true; } DUChain* DUChain::self() { return sdDUChainPrivate->instance; } extern void initModificationRevisionSetRepository(); extern void initDeclarationRepositories(); extern void initIdentifierRepository(); extern void initTypeRepository(); extern void initInstantiationInformationRepository(); QString DUChain::repositoryPathForSession(const KDevelop::ISessionLock::Ptr& session) { QString cacheDir = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation); cacheDir += QStringLiteral("/kdevduchain"); QString baseDir = QProcessEnvironment::systemEnvironment().value(QStringLiteral("KDEV_DUCHAIN_DIR"), cacheDir); baseDir += QStringLiteral("/%1-%2").arg(qApp->applicationName(), session->id()); return baseDir; } void DUChain::initialize() { // Initialize the global item repository as first thing after loading the session Q_ASSERT(ICore::self()); Q_ASSERT(ICore::self()->activeSession()); ItemRepositoryRegistry::initialize(repositoryPathForSession(ICore::self()->activeSessionLock())); initReferenceCounting(); // This needs to be initialized here too as the function is not threadsafe, but can // sometimes be called from different threads. This results in the underlying QFile // being 0 and hence crashes at some point later when accessing the contents via // read. See https://bugs.kde.org/show_bug.cgi?id=250779 RecursiveImportRepository::repository(); RecursiveImportCacheRepository::repository(); // similar to above, see https://bugs.kde.org/show_bug.cgi?id=255323 initDeclarationRepositories(); initModificationRevisionSetRepository(); initIdentifierRepository(); initTypeRepository(); initInstantiationInformationRepository(); Importers::self(); globalImportIdentifier(); globalIndexedImportIdentifier(); globalAliasIdentifier(); globalIndexedAliasIdentifier(); } DUChainLock* DUChain::lock() { return &sdDUChainPrivate->lock; } QList DUChain::allChains() const { QMutexLocker l(&sdDUChainPrivate->m_chainsMutex); return sdDUChainPrivate->m_chainsByUrl.values(); } void DUChain::updateContextEnvironment( TopDUContext* context, ParsingEnvironmentFile* file ) { QMutexLocker l(&sdDUChainPrivate->m_chainsMutex); removeFromEnvironmentManager( context ); context->setParsingEnvironmentFile( file ); addToEnvironmentManager( context ); } void DUChain::removeDocumentChain( TopDUContext* context ) { ENSURE_CHAIN_WRITE_LOCKED; IndexedTopDUContext indexed(context->indexed()); Q_ASSERT(indexed.data() == context); ///This assertion fails if you call removeDocumentChain(..) on a document that has not been added to the du-chain context->m_dynamicData->deleteOnDisk(); Q_ASSERT(indexed.data() == context); sdDUChainPrivate->removeDocumentChainFromMemory(context); Q_ASSERT(!indexed.data()); Q_ASSERT(!environmentFileForDocument(indexed)); QMutexLocker lock(&sdDUChainPrivate->m_chainsMutex); sdDUChainPrivate->m_availableTopContextIndices.push_back(indexed.index()); } void DUChain::addDocumentChain( TopDUContext * chain ) { QMutexLocker l(&sdDUChainPrivate->m_chainsMutex); // qCDebug(LANGUAGE) << "duchain: adding document" << chain->url().str() << " " << chain; Q_ASSERT(chain); Q_ASSERT(!sdDUChainPrivate->hasChainForIndex(chain->ownIndex())); { QMutexLocker lock(&DUChain::chainsByIndexLock); if(DUChain::chainsByIndex.size() <= chain->ownIndex()) DUChain::chainsByIndex.resize(chain->ownIndex() + 100, nullptr); DUChain::chainsByIndex[chain->ownIndex()] = chain; } { Q_ASSERT(DUChain::chainsByIndex[chain->ownIndex()]); } Q_ASSERT(sdDUChainPrivate->hasChainForIndex(chain->ownIndex())); sdDUChainPrivate->m_chainsByUrl.insert(chain->url(), chain); Q_ASSERT(sdDUChainPrivate->hasChainForIndex(chain->ownIndex())); chain->setInDuChain(true); l.unlock(); addToEnvironmentManager(chain); // This function might be called during shutdown by stale parse jobs // Make sure we don't access null-pointers here if (ICore::self() && ICore::self()->languageController() && ICore::self()->languageController()->backgroundParser()->trackerForUrl(chain->url())) { //Make sure the context stays alive at least as long as the context is open ReferencedTopDUContext ctx(chain); sdDUChainPrivate->m_openDocumentContexts.insert(ctx); } } void DUChain::addToEnvironmentManager( TopDUContext * chain ) { ParsingEnvironmentFilePointer file = chain->parsingEnvironmentFile(); if( !file ) return; //We don't need to manage Q_ASSERT(file->indexedTopContext().index() == chain->ownIndex()); if(ParsingEnvironmentFile* alreadyHave = sdDUChainPrivate->findInformation(file->indexedTopContext().index())) { ///If this triggers, there has already been another environment-information registered for this top-context. ///removeFromEnvironmentManager should have been called before to remove the old environment-information. Q_ASSERT(alreadyHave == file.data()); Q_UNUSED(alreadyHave); return; } sdDUChainPrivate->addEnvironmentInformation(file); } void DUChain::removeFromEnvironmentManager( TopDUContext * chain ) { ParsingEnvironmentFilePointer file = chain->parsingEnvironmentFile(); if( !file ) return; //We don't need to manage sdDUChainPrivate->removeEnvironmentInformation(file); } TopDUContext* DUChain::chainForDocument(const QUrl& document, bool proxyContext) const { return chainForDocument(IndexedString(document), proxyContext); } bool DUChain::isInMemory(uint topContextIndex) const { return DUChainPrivate::hasChainForIndex(topContextIndex); } IndexedString DUChain::urlForIndex(uint index) const { { TopDUContext* chain = DUChainPrivate::readChainForIndex(index); if(chain) return chain->url(); } return TopDUContextDynamicData::loadUrl(index); } TopDUContext* DUChain::loadChain(uint index) { QSet loaded; sdDUChainPrivate->loadChain(index, loaded); { QMutexLocker lock(&chainsByIndexLock); if(chainsByIndex.size() > index) { TopDUContext* top = chainsByIndex[index]; if(top) return top; } } return nullptr; } TopDUContext* DUChain::chainForDocument(const KDevelop::IndexedString& document, bool proxyContext) const { ENSURE_CHAIN_READ_LOCKED; if(sdDUChainPrivate->m_destroyed) return nullptr; const QList list = sdDUChainPrivate->getEnvironmentInformation(document); for (const ParsingEnvironmentFilePointer&file : list) { if(isInMemory(file->indexedTopContext().index()) && file->isProxyContext() == proxyContext) { return file->topContext(); } } for (const ParsingEnvironmentFilePointer& file : list) { if(proxyContext == file->isProxyContext()) { return file->topContext(); } } //Allow selecting a top-context even if there is no ParsingEnvironmentFile const QList ret = chainsForDocument(document); for (TopDUContext* ctx : ret) { if(!ctx->parsingEnvironmentFile() || (ctx->parsingEnvironmentFile()->isProxyContext() == proxyContext)) return ctx; } return nullptr; } QList DUChain::chainsForDocument(const QUrl& document) const { return chainsForDocument(IndexedString(document)); } QList DUChain::chainsForDocument(const IndexedString& document) const { QList chains; if(sdDUChainPrivate->m_destroyed) return chains; QMutexLocker l(&sdDUChainPrivate->m_chainsMutex); // Match all parsed versions of this document for (auto it = sdDUChainPrivate->m_chainsByUrl.lowerBound(document); it != sdDUChainPrivate->m_chainsByUrl.end(); ++it) { if (it.key() == document) chains << it.value(); else break; } return chains; } TopDUContext* DUChain::chainForDocument( const QUrl& document, const KDevelop::ParsingEnvironment* environment, bool proxyContext ) const { return chainForDocument( IndexedString(document), environment, proxyContext ); } ParsingEnvironmentFilePointer DUChain::environmentFileForDocument( const IndexedString& document, const ParsingEnvironment* environment, bool proxyContext ) const { ENSURE_CHAIN_READ_LOCKED; if(sdDUChainPrivate->m_destroyed) return ParsingEnvironmentFilePointer(); QList< ParsingEnvironmentFilePointer> list = sdDUChainPrivate->getEnvironmentInformation(document); // qCDebug(LANGUAGE) << document.str() << ": matching" << list.size() << (onlyProxyContexts ? "proxy-contexts" : (noProxyContexts ? "content-contexts" : "contexts")); auto it = list.constBegin(); while(it != list.constEnd()) { if(*it && ((*it)->isProxyContext() == proxyContext) && (*it)->matchEnvironment(environment) && // Verify that the environment-file and its top-context are "good": The top-context must exist, // and there must be a content-context associated to the proxy-context. (*it)->topContext() && (!proxyContext || DUChainUtils::contentContextFromProxyContext((*it)->topContext())) ) { return *it; } ++it; } return ParsingEnvironmentFilePointer(); } QList DUChain::allEnvironmentFiles(const IndexedString& document) { return sdDUChainPrivate->getEnvironmentInformation(document); } ParsingEnvironmentFilePointer DUChain::environmentFileForDocument(IndexedTopDUContext topContext) const { if(topContext.index() == 0) return ParsingEnvironmentFilePointer(); return ParsingEnvironmentFilePointer(sdDUChainPrivate->loadInformation(topContext.index())); } TopDUContext* DUChain::chainForDocument( const IndexedString& document, const ParsingEnvironment* environment, bool proxyContext ) const { if(sdDUChainPrivate->m_destroyed) return nullptr; ParsingEnvironmentFilePointer envFile = environmentFileForDocument(document, environment, proxyContext); if(envFile) { return envFile->topContext(); }else{ return nullptr; } } QList DUChain::documents() const { QMutexLocker l(&sdDUChainPrivate->m_chainsMutex); QList ret; ret.reserve(sdDUChainPrivate->m_chainsByUrl.count()); foreach(TopDUContext* top, sdDUChainPrivate->m_chainsByUrl) { ret << top->url().toUrl(); } return ret; } QList DUChain::indexedDocuments() const { QMutexLocker l(&sdDUChainPrivate->m_chainsMutex); QList ret; ret.reserve(sdDUChainPrivate->m_chainsByUrl.count()); foreach(TopDUContext* top, sdDUChainPrivate->m_chainsByUrl) { ret << top->url(); } return ret; } void DUChain::documentActivated(KDevelop::IDocument* doc) { if(sdDUChainPrivate->m_destroyed) return; DUChainReadLocker lock( DUChain::lock() ); QMutexLocker l(&sdDUChainPrivate->m_chainsMutex); auto backgroundParser = ICore::self()->languageController()->backgroundParser(); auto addWithHighPriority = [backgroundParser, doc]() { backgroundParser->addDocument(IndexedString(doc->url()), TopDUContext::VisibleDeclarationsAndContexts, BackgroundParser::BestPriority); }; TopDUContext* ctx = DUChainUtils::standardContextForUrl(doc->url(), true); //Check whether the document has an attached environment-manager, and whether that one thinks the document needs to be updated. //If yes, update it. if (ctx && ctx->parsingEnvironmentFile() && ctx->parsingEnvironmentFile()->needsUpdate()) { qCDebug(LANGUAGE) << "Document needs update, using best priority since it just got activated:" << doc->url(); addWithHighPriority(); } else if (backgroundParser->managedDocuments().contains(IndexedString(doc->url()))) { // increase priority if there's already parse job of this document in the queue qCDebug(LANGUAGE) << "Prioritizing activated document:" << doc->url(); addWithHighPriority(); } } void DUChain::documentClosed(IDocument* document) { if(sdDUChainPrivate->m_destroyed) return; IndexedString url(document->url()); foreach(const ReferencedTopDUContext &top, sdDUChainPrivate->m_openDocumentContexts) if(top->url() == url) sdDUChainPrivate->m_openDocumentContexts.remove(top); } void DUChain::documentLoadedPrepare(KDevelop::IDocument* doc) { if(sdDUChainPrivate->m_destroyed) return; const IndexedString url(doc->url()); DUChainWriteLocker lock( DUChain::lock() ); QMutexLocker l(&sdDUChainPrivate->m_chainsMutex); TopDUContext* standardContext = DUChainUtils::standardContextForUrl(doc->url()); QList chains = chainsForDocument(url); const auto languages = ICore::self()->languageController()->languagesForUrl(doc->url()); if(standardContext) { Q_ASSERT(chains.contains(standardContext)); //We have just loaded it Q_ASSERT((standardContext->url() == url)); sdDUChainPrivate->m_openDocumentContexts.insert(standardContext); bool needsUpdate = standardContext->parsingEnvironmentFile() && standardContext->parsingEnvironmentFile()->needsUpdate(); if(!needsUpdate) { //Only apply the highlighting if we don't need to update, else we might highlight total crap //Do instant highlighting only if all imports are loaded, to make sure that we don't block the user-interface too long //Else the highlighting will be done in the background-thread //This is not exactly right, as the direct imports don't necessarily equal the real imports used by uses //but it approximates the correct behavior. bool allImportsLoaded = true; foreach(const DUContext::Import& import, standardContext->importedParentContexts()) if(!import.indexedContext().indexedTopContext().isLoaded()) allImportsLoaded = false; if(allImportsLoaded) { l.unlock(); lock.unlock(); for (const auto language : languages) { if(language->codeHighlighting()) { language->codeHighlighting()->highlightDUChain(standardContext); } } qCDebug(LANGUAGE) << "highlighted" << doc->url() << "in foreground"; return; } }else{ qCDebug(LANGUAGE) << "not highlighting the duchain because the documents needs an update"; } if(needsUpdate || !(standardContext->features() & TopDUContext::AllDeclarationsContextsAndUses)) { ICore::self()->languageController()->backgroundParser()->addDocument(IndexedString(doc->url()), (TopDUContext::Features)(TopDUContext::AllDeclarationsContextsAndUses | TopDUContext::ForceUpdate)); return; } } //Add for highlighting etc. ICore::self()->languageController()->backgroundParser()->addDocument(IndexedString(doc->url()), TopDUContext::AllDeclarationsContextsAndUses); } void DUChain::documentRenamed(KDevelop::IDocument* doc) { if(sdDUChainPrivate->m_destroyed) return; if(!doc->url().isValid()) { ///Maybe this happens when a file was deleted? qCWarning(LANGUAGE) << "Strange, url of renamed document is invalid!"; }else{ ICore::self()->languageController()->backgroundParser()->addDocument(IndexedString(doc->url()), (TopDUContext::Features)(TopDUContext::AllDeclarationsContextsAndUses | TopDUContext::ForceUpdate)); } } Uses* DUChain::uses() { return &sdDUChainPrivate->m_uses; } Definitions* DUChain::definitions() { return &sdDUChainPrivate->m_definitions; } static void finalCleanup() { DUChainWriteLocker writeLock(DUChain::lock()); qCDebug(LANGUAGE) << "doing final cleanup"; int cleaned = 0; while((cleaned = globalItemRepositoryRegistry().finalCleanup())) { qCDebug(LANGUAGE) << "cleaned" << cleaned << "B"; if(cleaned < 1000) { qCDebug(LANGUAGE) << "cleaned enough"; break; } } qCDebug(LANGUAGE) << "final cleanup ready"; } void DUChain::shutdown() { // if core is not shutting down, we can end up in deadlocks or crashes // since language plugins might still try to access static duchain stuff Q_ASSERT(!ICore::self() || ICore::self()->shuttingDown()); qCDebug(LANGUAGE) << "Cleaning up and shutting down DUChain"; QMutexLocker lock(&sdDUChainPrivate->cleanupMutex()); { //Acquire write-lock of the repository, so when kdevelop crashes in that process, the repository is discarded //Crashes here may happen in an inconsistent state, thus this makes sense, to protect the user from more crashes globalItemRepositoryRegistry().lockForWriting(); sdDUChainPrivate->cleanupTopContexts(); globalItemRepositoryRegistry().unlockForWriting(); } sdDUChainPrivate->doMoreCleanup(); //Must be done _before_ finalCleanup, else we may be deleting yet needed data sdDUChainPrivate->m_openDocumentContexts.clear(); sdDUChainPrivate->m_destroyed = true; sdDUChainPrivate->clear(); { //Acquire write-lock of the repository, so when kdevelop crashes in that process, the repository is discarded //Crashes here may happen in an inconsistent state, thus this makes sense, to protect the user from more crashes globalItemRepositoryRegistry().lockForWriting(); finalCleanup(); globalItemRepositoryRegistry().unlockForWriting(); } globalItemRepositoryRegistry().shutdown(); } uint DUChain::newTopContextIndex() { { QMutexLocker lock(&sdDUChainPrivate->m_chainsMutex); if(!sdDUChainPrivate->m_availableTopContextIndices.isEmpty()) { uint ret = sdDUChainPrivate->m_availableTopContextIndices.back(); sdDUChainPrivate->m_availableTopContextIndices.pop_back(); if(TopDUContextDynamicData::fileExists(ret)) { qCWarning(LANGUAGE) << "Problem in the management of available top-context indices"; return newTopContextIndex(); } return ret; } } static QAtomicInt& currentId( globalItemRepositoryRegistry().customCounter(QStringLiteral("Top-Context Counter"), 1) ); return currentId.fetchAndAddRelaxed(1); } void DUChain::refCountUp(TopDUContext* top) { QMutexLocker l(&sdDUChainPrivate->m_referenceCountsMutex); // note: value is default-constructed to zero if it does not exist ++sdDUChainPrivate->m_referenceCounts[top]; } bool DUChain::deleted() { return m_deleted; } void DUChain::refCountDown(TopDUContext* top) { QMutexLocker l(&sdDUChainPrivate->m_referenceCountsMutex); auto it = sdDUChainPrivate->m_referenceCounts.find(top); if (it == sdDUChainPrivate->m_referenceCounts.end()) { //qCWarning(LANGUAGE) << "tried to decrease reference-count for" << top->url().str() << "but this top-context is not referenced"; return; } auto& refCount = *it; --refCount; if (!refCount) { sdDUChainPrivate->m_referenceCounts.erase(it); } } void DUChain::emitDeclarationSelected(const DeclarationPointer& decl) { if(sdDUChainPrivate->m_destroyed) return; emit declarationSelected(decl); } void DUChain::emitUpdateReady(const IndexedString& url, const ReferencedTopDUContext& topContext) { if(sdDUChainPrivate->m_destroyed) return; emit updateReady(url, topContext); } KDevelop::ReferencedTopDUContext DUChain::waitForUpdate(const KDevelop::IndexedString& document, KDevelop::TopDUContext::Features minFeatures, bool proxyContext) { Q_ASSERT(!lock()->currentThreadHasReadLock() && !lock()->currentThreadHasWriteLock()); WaitForUpdate waiter; updateContextForUrl(document, minFeatures, &waiter); while(!waiter.m_ready) { // we might have been shut down in the meanwhile if (!ICore::self()) { return nullptr; } QMetaObject::invokeMethod(ICore::self()->languageController()->backgroundParser(), "parseDocuments"); QApplication::processEvents(); QThread::usleep(1000); } if(!proxyContext) { DUChainReadLocker readLock(DUChain::lock()); return DUChainUtils::contentContextFromProxyContext(waiter.m_topContext); } return waiter.m_topContext; } void DUChain::updateContextForUrl(const IndexedString& document, TopDUContext::Features minFeatures, QObject* notifyReady, int priority) const { DUChainReadLocker lock( DUChain::lock() ); TopDUContext* standardContext = DUChainUtils::standardContextForUrl(document.toUrl()); if(standardContext && standardContext->parsingEnvironmentFile() && !standardContext->parsingEnvironmentFile()->needsUpdate() && standardContext->parsingEnvironmentFile()->featuresSatisfied(minFeatures)) { lock.unlock(); if(notifyReady) QMetaObject::invokeMethod(notifyReady, "updateReady", Qt::DirectConnection, Q_ARG(KDevelop::IndexedString, document), Q_ARG(KDevelop::ReferencedTopDUContext, ReferencedTopDUContext(standardContext))); }else{ ///Start a parse-job for the given document ICore::self()->languageController()->backgroundParser()->addDocument(document, minFeatures, priority, notifyReady); } } void DUChain::disablePersistentStorage(bool disable) { sdDUChainPrivate->m_cleanupDisabled = disable; } void DUChain::storeToDisk() { bool wasDisabled = sdDUChainPrivate->m_cleanupDisabled; sdDUChainPrivate->m_cleanupDisabled = false; sdDUChainPrivate->doMoreCleanup(); sdDUChainPrivate->m_cleanupDisabled = wasDisabled; } bool DUChain::compareToDisk() { DUChainWriteLocker writeLock(DUChain::lock()); ///Step 1: Compare the repositories return true; } } diff --git a/kdevplatform/shell/configdialog.cpp b/kdevplatform/shell/configdialog.cpp index 6ad82220e8..3afc8e544c 100644 --- a/kdevplatform/shell/configdialog.cpp +++ b/kdevplatform/shell/configdialog.cpp @@ -1,216 +1,216 @@ /* * This file is part of KDevelop * Copyright 2014 Alex Richardson * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) version 3, or any * later version accepted by the membership of KDE e.V. (or its * successor approved by the membership of KDE e.V.), which shall * act as a proxy defined in Section 6 of version 3 of the license. * * 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . */ #include "configdialog.h" #include "debug.h" #include #include #include #include #include #include #include #include #include using namespace KDevelop; //FIXME: unit test this code! ConfigDialog::ConfigDialog(QWidget* parent) : KPageDialog(parent) { setWindowTitle(i18n("Configure")); setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Apply | QDialogButtonBox::Cancel | QDialogButtonBox::RestoreDefaults); button(QDialogButtonBox::Apply)->setEnabled(false); setObjectName(QStringLiteral("configdialog")); auto onApplyClicked = [this] { auto page = qobject_cast(currentPage()->widget()); Q_ASSERT(page); applyChanges(page); }; connect(button(QDialogButtonBox::Apply), &QPushButton::clicked, onApplyClicked); connect(button(QDialogButtonBox::Ok), &QPushButton::clicked, onApplyClicked); - connect(button(QDialogButtonBox::RestoreDefaults), &QPushButton::clicked, [this]() { + connect(button(QDialogButtonBox::RestoreDefaults), &QPushButton::clicked, this, [this]() { auto page = qobject_cast(currentPage()->widget()); Q_ASSERT(page); page->defaults(); }); connect(this, &KPageDialog::currentPageChanged, this, &ConfigDialog::checkForUnsavedChanges); // make sure we don't keep any entries for unloaded plugins connect(ICore::self()->pluginController(), &IPluginController::unloadingPlugin, this, &ConfigDialog::removePagesForPlugin); } KPageWidgetItem* ConfigDialog::itemForPage(ConfigPage* page) const { for (auto& item : m_pages) { if (item->widget() == page) { return item; } } return nullptr; } int ConfigDialog::checkForUnsavedChanges(KPageWidgetItem* current, KPageWidgetItem* before) { Q_UNUSED(current); if (!m_currentPageHasChanges) { return KMessageBox::Yes; } // before must be non-null, because if we change from nothing to a new page m_currentPageHasChanges must also be false! Q_ASSERT(before); auto oldPage = qobject_cast(before->widget()); Q_ASSERT(oldPage); auto dialogResult = KMessageBox::warningYesNoCancel(this, i18n("The settings of the current module have changed.\n" "Do you want to apply the changes or discard them?"), i18n("Apply Settings"), KStandardGuiItem::apply(), KStandardGuiItem::discard(), KStandardGuiItem::cancel()); if (dialogResult == KMessageBox::No) { oldPage->reset(); m_currentPageHasChanges = false; button(QDialogButtonBox::Apply)->setEnabled(false); } else if (dialogResult == KMessageBox::Yes) { applyChanges(oldPage); } else if (dialogResult == KMessageBox::Cancel) { // restore old state QSignalBlocker block(this); // prevent recursion setCurrentPage(before); } return dialogResult; } void ConfigDialog::closeEvent(QCloseEvent* event) { if (checkForUnsavedChanges(currentPage(), currentPage()) == KMessageBox::Cancel) { // if the user clicked cancel he wants to continue editing the current page -> don't close event->ignore(); } else { event->accept(); } } void ConfigDialog::removeConfigPage(ConfigPage* page) { auto item = itemForPage(page); Q_ASSERT(item); removePage(item); m_pages.removeAll(QPointer(item)); // also remove all items that were deleted because a parent KPageWidgetItem was removed m_pages.removeAll(QPointer()); } void ConfigDialog::removePagesForPlugin(IPlugin* plugin) { Q_ASSERT(plugin); foreach (auto&& item, m_pages) { if (!item) { continue; } auto page = qobject_cast(item->widget()); if (page && page->plugin() == plugin) { removePage(item); // this also deletes the config page -> QPointer is set to null } }; // also remove all items that were deleted because a parent KPageWidgetItem was removed m_pages.removeAll(QPointer()); } void ConfigDialog::appendConfigPage(ConfigPage* page) { addConfigPageInternal(addPage(page, page->name()), page); } void ConfigDialog::insertConfigPage(ConfigPage* before, ConfigPage* page) { Q_ASSERT(before); auto beforeItem = itemForPage(before); Q_ASSERT(beforeItem); addConfigPageInternal(insertPage(beforeItem, page, page->name()), page); } void ConfigDialog::appendSubConfigPage(ConfigPage* parentPage, ConfigPage* page) { auto item = itemForPage(parentPage); Q_ASSERT(item); addConfigPageInternal(addSubPage(item, page, page->name()), page); } void ConfigDialog::addConfigPageInternal(KPageWidgetItem* item, ConfigPage* page) { item->setHeader(page->fullName()); item->setIcon(page->icon()); page->initConfigManager(); page->reset(); // make sure all widgets are in the correct state // make sure that we only connect to changed after calling reset() connect(page, &ConfigPage::changed, this, &ConfigDialog::onPageChanged); m_pages.append(item); for (int i = 0; i < page->childPages(); ++i) { auto child = page->childPage(i); appendSubConfigPage(page, child); } } void ConfigDialog::onPageChanged() { QObject* from = sender(); if (from && from != currentPage()->widget()) { qCWarning(SHELL) << "Settings in config page" << from << "changed, while" << currentPage()->widget() << "is currently selected. This case is not implemented yet."; return; // TODO: add a QHash as a member to make sure the apply button is always correct // TODO: when pressing okay show confirm dialog if other pages have changed or just silently apply every page? "Items on other pages have changed, do you wish to review those changes? + list with changed pages." } if (!m_currentlyApplyingChanges) { // e.g. PluginPreferences emits changed() from its apply method, better fix this here than having to // ensure that no plugin emits changed() from apply() // together with KPageDialog emitting currentPageChanged("Plugins", nullptr) this could cause a crash // when we dereference before m_currentPageHasChanges = true; button(QDialogButtonBox::Apply)->setEnabled(true); } } void ConfigDialog::applyChanges(ConfigPage* page) { // must set this to false before calling apply, otherwise we get the confirmation dialog // whenever we enable/disable plugins. // This is because KPageWidget then emits currentPageChanged("Plugins", nullptr), which seems like a bug to me, // it should rather emit currentPageChanged("Plugins", "Plugins") or even better nothing at all, since the current // page did not actually change! // TODO: fix KPageWidget m_currentPageHasChanges = false; m_currentlyApplyingChanges = true; page->apply(); m_currentlyApplyingChanges = false; Q_ASSERT(!m_currentPageHasChanges); button(QDialogButtonBox::Apply)->setEnabled(false); emit configSaved(page); } diff --git a/kdevplatform/shell/projectcontroller.cpp b/kdevplatform/shell/projectcontroller.cpp index c5a42fb4c7..692c098a7a 100644 --- a/kdevplatform/shell/projectcontroller.cpp +++ b/kdevplatform/shell/projectcontroller.cpp @@ -1,1283 +1,1283 @@ /* This file is part of KDevelop Copyright 2006 Adam Treat Copyright 2007 Andreas Pakulat 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "projectcontroller.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "core.h" // TODO: Should get rid off this include (should depend on IProject only) #include "project.h" #include "mainwindow.h" #include "shellextension.h" #include "plugincontroller.h" #include "configdialog.h" #include "uicontroller.h" #include "documentcontroller.h" #include "openprojectdialog.h" #include "sessioncontroller.h" #include "session.h" #include "debug.h" namespace KDevelop { class ProjectControllerPrivate { public: QList m_projects; QMap< IProject*, QList > m_projectPlugins; QPointer m_recentProjectsAction; Core* m_core; // IProject* m_currentProject; ProjectModel* model; QPointer m_openProject; QPointer m_fetchProject; QPointer m_closeProject; QPointer m_openConfig; IProjectDialogProvider* dialog; QList m_currentlyOpening; // project-file urls that are being opened ProjectController* q; ProjectBuildSetModel* buildset; bool m_foundProjectFile; //Temporary flag used while searching the hierarchy for a project file bool m_cleaningUp; //Temporary flag enabled while destroying the project-controller ProjectChangesModel* m_changesModel = nullptr; QHash< IProject*, QPointer > m_parseJobs; // parse jobs that add files from the project to the background parser. explicit ProjectControllerPrivate( ProjectController* p ) : m_core(nullptr), model(nullptr), dialog(nullptr), q(p), buildset(nullptr), m_foundProjectFile(false), m_cleaningUp(false) { } void unloadAllProjectPlugins() { if( m_projects.isEmpty() ) m_core->pluginControllerInternal()->unloadProjectPlugins(); } void projectConfig( QObject * obj ) { if( !obj ) return; Project* proj = qobject_cast(obj); if( !proj ) return; auto cfgDlg = new KDevelop::ConfigDialog(m_core->uiController()->activeMainWindow()); cfgDlg->setAttribute(Qt::WA_DeleteOnClose); cfgDlg->setModal(true); QVector configPages; ProjectConfigOptions options; options.developerFile = proj->developerFile(); options.developerTempFile = proj->developerTempFile(); options.projectTempFile = proj->projectTempFile(); options.project = proj; foreach (IPlugin* plugin, findPluginsForProject(proj)) { const int perProjectConfigPagesCount = plugin->perProjectConfigPages(); configPages.reserve(configPages.size() + perProjectConfigPagesCount); for (int i = 0; i < perProjectConfigPagesCount; ++i) { configPages.append(plugin->perProjectConfigPage(i, options, cfgDlg)); } } std::sort(configPages.begin(), configPages.end(), [](const ConfigPage* a, const ConfigPage* b) { return a->name() < b->name(); }); for (auto page : configPages) { cfgDlg->appendConfigPage(page); } QObject::connect(cfgDlg, &ConfigDialog::configSaved, cfgDlg, [this, proj](ConfigPage* page) { Q_UNUSED(page) Q_ASSERT_X(proj, Q_FUNC_INFO, "ConfigDialog signalled project config change, but no project set for configuring!"); emit q->projectConfigurationChanged(proj); }); cfgDlg->setWindowTitle(i18n("Configure Project %1", proj->name())); - QObject::connect(cfgDlg, &KDevelop::ConfigDialog::finished, [proj]() { + QObject::connect(cfgDlg, &KDevelop::ConfigDialog::finished, proj, [proj]() { proj->projectConfiguration()->sync(); }); cfgDlg->show(); } void saveListOfOpenedProjects() { auto activeSession = Core::self()->activeSession(); if (!activeSession) { return; } QList openProjects; openProjects.reserve( m_projects.size() ); foreach( IProject* project, m_projects ) { openProjects.append(project->projectFile().toUrl()); } activeSession->setContainedProjects( openProjects ); } // Recursively collects builder dependencies for a project. static void collectBuilders( QList< IProjectBuilder* >& destination, IProjectBuilder* topBuilder, IProject* project ) { const QList auxBuilders = topBuilder->additionalBuilderPlugins(project); destination.append( auxBuilders ); for (IProjectBuilder* auxBuilder : auxBuilders ) { collectBuilders( destination, auxBuilder, project ); } } QVector findPluginsForProject( IProject* project ) const { const QList plugins = m_core->pluginController()->loadedPlugins(); QVector projectPlugins; QList buildersForKcm; // Important to also include the "top" builder for the project, so // projects with only one such builder are kept working. Otherwise the project config // dialog is empty for such cases. if( IBuildSystemManager* buildSystemManager = project->buildSystemManager() ) { buildersForKcm << buildSystemManager->builder(); collectBuilders( buildersForKcm, buildSystemManager->builder(), project ); } for (auto plugin : plugins) { auto info = m_core->pluginController()->pluginInfo(plugin); IProjectFileManager* manager = plugin->extension(); if( manager && manager != project->projectFileManager() ) { // current plugin is a manager but does not apply to given project, skip continue; } IProjectBuilder* builder = plugin->extension(); if ( builder && !buildersForKcm.contains( builder ) ) { continue; } qCDebug(SHELL) << "Using plugin" << info.pluginId() << "for project" << project->name(); projectPlugins << plugin; } return projectPlugins; } void updateActionStates() { // if only one project loaded, this is always our target int itemCount = (m_projects.size() == 1) ? 1 : 0; if (itemCount == 0) { // otherwise base on selection ProjectItemContext* itemContext = dynamic_cast(ICore::self()->selectionController()->currentSelection()); if (itemContext) { itemCount = itemContext->items().count(); } } m_openConfig->setEnabled(itemCount == 1); m_closeProject->setEnabled(itemCount > 0); } void openProjectConfig() { // if only one project loaded, this is our target IProject *project = (m_projects.count() == 1) ? m_projects.at(0) : nullptr; // otherwise base on selection if (!project) { ProjectItemContext* ctx = dynamic_cast(ICore::self()->selectionController()->currentSelection()); if (ctx && ctx->items().count() == 1) { project = ctx->items().at(0)->project(); } } if (project) { q->configureProject(project); } } void closeSelectedProjects() { QSet projects; // if only one project loaded, this is our target if (m_projects.count() == 1) { projects.insert(m_projects.at(0)); } else { // otherwise base on selection ProjectItemContext* ctx = dynamic_cast(ICore::self()->selectionController()->currentSelection()); if (ctx) { foreach (ProjectBaseItem* item, ctx->items()) { projects.insert(item->project()); } } } foreach (IProject* project, projects) { q->closeProject(project); } } void importProject(const QUrl& url_) { QUrl url(url_); if (url.isLocalFile()) { const QString path = QFileInfo(url.toLocalFile()).canonicalFilePath(); if (!path.isEmpty()) { url = QUrl::fromLocalFile(path); } } if ( !url.isValid() ) { KMessageBox::error(Core::self()->uiControllerInternal()->activeMainWindow(), i18n("Invalid Location: %1", url.toDisplayString(QUrl::PreferLocalFile))); return; } if ( m_currentlyOpening.contains(url)) { qCDebug(SHELL) << "Already opening " << url << ". Aborting."; KPassivePopup::message( i18n( "Project already being opened"), i18n( "Already opening %1, not opening again", url.toDisplayString(QUrl::PreferLocalFile) ), m_core->uiController()->activeMainWindow() ); return; } foreach( IProject* project, m_projects ) { if ( url == project->projectFile().toUrl() ) { if ( dialog->userWantsReopen() ) { // close first, then open again by falling through q->closeProject(project); } else { // abort return; } } } m_currentlyOpening += url; m_core->pluginControllerInternal()->loadProjectPlugins(); Project* project = new Project(); QObject::connect(project, &Project::aboutToOpen, q, &ProjectController::projectAboutToBeOpened); if ( !project->open( Path(url) ) ) { m_currentlyOpening.removeAll(url); q->abortOpeningProject(project); project->deleteLater(); } } void areaChanged(Sublime::Area* area) { KActionCollection* ac = m_core->uiControllerInternal()->defaultMainWindow()->actionCollection(); ac->action(QStringLiteral("commit_current_project"))->setEnabled(area->objectName() == QLatin1String("code")); ac->action(QStringLiteral("commit_current_project"))->setVisible(area->objectName() == QLatin1String("code")); } }; IProjectDialogProvider::IProjectDialogProvider() {} IProjectDialogProvider::~IProjectDialogProvider() {} ProjectDialogProvider::ProjectDialogProvider(ProjectControllerPrivate* const p) : d(p) {} ProjectDialogProvider::~ProjectDialogProvider() {} bool writeNewProjectFile( const QString& localConfigFile, const QString& name, const QString& createdFrom, const QString& manager ) { KSharedConfigPtr cfg = KSharedConfig::openConfig( localConfigFile, KConfig::SimpleConfig ); if (!cfg->isConfigWritable(true)) { qCDebug(SHELL) << "can't write to configfile"; return false; } KConfigGroup grp = cfg->group( "Project" ); grp.writeEntry( "Name", name ); grp.writeEntry( "CreatedFrom", createdFrom ); grp.writeEntry( "Manager", manager ); cfg->sync(); return true; } bool writeProjectSettingsToConfigFile(const QUrl& projectFileUrl, OpenProjectDialog* dlg) { if ( !projectFileUrl.isLocalFile() ) { QTemporaryFile tmp; if ( !tmp.open() ) { return false; } if ( !writeNewProjectFile( tmp.fileName(), dlg->projectName(), dlg->selectedUrl().fileName(), dlg->projectManager() ) ) { return false; } // explicitly close file before uploading it, see also: https://bugs.kde.org/show_bug.cgi?id=254519 tmp.close(); auto uploadJob = KIO::file_copy(QUrl::fromLocalFile(tmp.fileName()), projectFileUrl); KJobWidgets::setWindow(uploadJob, Core::self()->uiControllerInternal()->defaultMainWindow()); return uploadJob->exec(); } // Here and above we take .filename() part of the selectedUrl() to make it relative to the project root, // thus keeping .kdev file relocatable return writeNewProjectFile( projectFileUrl.toLocalFile(), dlg->projectName(), dlg->selectedUrl().fileName(), dlg->projectManager() ); } bool projectFileExists( const QUrl& u ) { if( u.isLocalFile() ) { return QFileInfo::exists( u.toLocalFile() ); } else { auto statJob = KIO::stat(u, KIO::StatJob::DestinationSide, 0, KIO::HideProgressInfo); KJobWidgets::setWindow(statJob, Core::self()->uiControllerInternal()->activeMainWindow()); return statJob->exec(); } } bool equalProjectFile( const QString& configPath, OpenProjectDialog* dlg ) { KSharedConfigPtr cfg = KSharedConfig::openConfig( configPath, KConfig::SimpleConfig ); KConfigGroup grp = cfg->group( "Project" ); QString defaultName = dlg->projectFileUrl().adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).fileName(); return (grp.readEntry( "Name", QString() ) == dlg->projectName() || dlg->projectName() == defaultName) && grp.readEntry( "Manager", QString() ) == dlg->projectManager(); } QUrl ProjectDialogProvider::askProjectConfigLocation(bool fetch, const QUrl& startUrl, const QUrl& repoUrl, IPlugin* vcsOrProviderPlugin) { Q_ASSERT(d); ScopedDialog dlg(fetch, startUrl, repoUrl, vcsOrProviderPlugin, Core::self()->uiController()->activeMainWindow()); if(dlg->exec() == QDialog::Rejected) { return QUrl(); } QUrl projectFileUrl = dlg->projectFileUrl(); qCDebug(SHELL) << "selected project:" << projectFileUrl << dlg->projectName() << dlg->projectManager(); if ( dlg->projectManager() == QLatin1String("") ) { return projectFileUrl; } // controls if existing project file should be saved bool writeProjectConfigToFile = true; if( projectFileExists( projectFileUrl ) ) { // check whether config is equal bool shouldAsk = true; if( projectFileUrl == dlg->selectedUrl() ) { if( projectFileUrl.isLocalFile() ) { shouldAsk = !equalProjectFile( projectFileUrl.toLocalFile(), dlg ); } else { shouldAsk = false; QTemporaryFile tmpFile; if (tmpFile.open()) { auto downloadJob = KIO::file_copy(projectFileUrl, QUrl::fromLocalFile(tmpFile.fileName())); KJobWidgets::setWindow(downloadJob, qApp->activeWindow()); if (downloadJob->exec()) { shouldAsk = !equalProjectFile(tmpFile.fileName(), dlg); } } } } if ( shouldAsk ) { KGuiItem yes = KStandardGuiItem::yes(); yes.setText(i18n("Override")); yes.setToolTip(i18nc("@info:tooltip", "Continue to open the project and use the just provided project configuration.")); yes.setIcon(QIcon()); KGuiItem no = KStandardGuiItem::no(); no.setText(i18n("Open Existing File")); no.setToolTip(i18nc("@info:tooltip", "Continue to open the project but use the existing project configuration.")); no.setIcon(QIcon()); KGuiItem cancel = KStandardGuiItem::cancel(); cancel.setToolTip(i18nc("@info:tooltip", "Cancel and do not open the project.")); int ret = KMessageBox::questionYesNoCancel(qApp->activeWindow(), i18n("There already exists a project configuration file at %1.\n" "Do you want to override it or open the existing file?", projectFileUrl.toDisplayString(QUrl::PreferLocalFile)), i18n("Override existing project configuration"), yes, no, cancel ); if ( ret == KMessageBox::No ) { writeProjectConfigToFile = false; } else if ( ret == KMessageBox::Cancel ) { return QUrl(); } // else fall through and write new file } else { writeProjectConfigToFile = false; } } if (writeProjectConfigToFile) { Path projectConfigDir(projectFileUrl); projectConfigDir.setLastPathSegment(QStringLiteral(".kdev4")); auto delJob = KIO::del(projectConfigDir.toUrl()); delJob->exec(); if (!writeProjectSettingsToConfigFile(projectFileUrl, dlg)) { KMessageBox::error(d->m_core->uiControllerInternal()->defaultMainWindow(), i18n("Unable to create configuration file %1", projectFileUrl.url())); return QUrl(); } } return projectFileUrl; } bool ProjectDialogProvider::userWantsReopen() { Q_ASSERT(d); return (KMessageBox::questionYesNo( d->m_core->uiControllerInternal()->defaultMainWindow(), i18n( "Reopen the current project?" ) ) == KMessageBox::No) ? false : true; } void ProjectController::setDialogProvider(IProjectDialogProvider* dialog) { Q_ASSERT(d->dialog); delete d->dialog; d->dialog = dialog; } ProjectController::ProjectController( Core* core ) : IProjectController( core ), d( new ProjectControllerPrivate( this ) ) { qRegisterMetaType>(); setObjectName(QStringLiteral("ProjectController")); d->m_core = core; d->model = new ProjectModel(); //NOTE: this is required to be called here, such that the // actions are available when the UI controller gets // initialized *before* the project controller if (Core::self()->setupFlags() != Core::NoUi) { setupActions(); } } void ProjectController::setupActions() { KActionCollection * ac = d->m_core->uiControllerInternal()->defaultMainWindow()->actionCollection(); QAction*action; d->m_openProject = action = ac->addAction( QStringLiteral("project_open") ); action->setText(i18nc( "@action", "Open / Import Project..." ) ); action->setToolTip( i18nc( "@info:tooltip", "Open or import project" ) ); action->setWhatsThis( i18nc( "@info:whatsthis", "Open an existing KDevelop 4 project or import " "an existing Project into KDevelop 4. This entry " "allows one to select a KDevelop4 project file " "or an existing directory to open it in KDevelop. " "When opening an existing directory that does " "not yet have a KDevelop4 project file, the file " "will be created." ) ); action->setIcon(QIcon::fromTheme(QStringLiteral("project-open"))); connect(action, &QAction::triggered, this, [&] { openProject(); }); d->m_fetchProject = action = ac->addAction( QStringLiteral("project_fetch") ); action->setText(i18nc( "@action", "Fetch Project..." ) ); action->setIcon( QIcon::fromTheme( QStringLiteral("edit-download") ) ); action->setToolTip( i18nc( "@info:tooltip", "Fetch project" ) ); action->setWhatsThis( i18nc( "@info:whatsthis", "Guides the user through the project fetch " "and then imports it into KDevelop 4." ) ); // action->setIcon(QIcon::fromTheme("project-open")); connect( action, &QAction::triggered, this, &ProjectController::fetchProject ); // action = ac->addAction( "project_close" ); // action->setText( i18n( "C&lose Project" ) ); // connect( action, SIGNAL(triggered(bool)), SLOT(closeProject()) ); // action->setToolTip( i18n( "Close project" ) ); // action->setWhatsThis( i18n( "Closes the current project." ) ); // action->setEnabled( false ); d->m_closeProject = action = ac->addAction( QStringLiteral("project_close") ); connect( action, &QAction::triggered, this, [&] { d->closeSelectedProjects(); } ); action->setText( i18nc( "@action", "Close Project(s)" ) ); action->setIcon( QIcon::fromTheme( QStringLiteral("project-development-close") ) ); action->setToolTip( i18nc( "@info:tooltip", "Closes all currently selected projects" ) ); action->setEnabled( false ); d->m_openConfig = action = ac->addAction( QStringLiteral("project_open_config") ); connect( action, &QAction::triggered, this, [&] { d->openProjectConfig(); } ); action->setText( i18n( "Open Configuration..." ) ); action->setIcon( QIcon::fromTheme(QStringLiteral("configure")) ); action->setEnabled( false ); action = ac->addAction( QStringLiteral("commit_current_project") ); connect( action, &QAction::triggered, this, &ProjectController::commitCurrentProject ); action->setText( i18n( "Commit Current Project..." ) ); action->setIconText( i18n( "Commit..." ) ); action->setIcon( QIcon::fromTheme(QStringLiteral("svn-commit")) ); connect(d->m_core->uiControllerInternal()->defaultMainWindow(), &MainWindow::areaChanged, this, [&] (Sublime::Area* area) { d->areaChanged(area); }); d->m_core->uiControllerInternal()->area(0, QStringLiteral("code"))->addAction(action); KSharedConfig * config = KSharedConfig::openConfig().data(); // KConfigGroup group = config->group( "General Options" ); d->m_recentProjectsAction = KStandardAction::openRecent(this, SLOT(openProject(QUrl)), this); ac->addAction( QStringLiteral("project_open_recent"), d->m_recentProjectsAction ); d->m_recentProjectsAction->setText( i18n( "Open Recent Project" ) ); d->m_recentProjectsAction->setWhatsThis( i18nc( "@info:whatsthis", "Opens recently opened project." ) ); d->m_recentProjectsAction->loadEntries( KConfigGroup(config, "RecentProjects") ); QAction* openProjectForFileAction = new QAction( this ); ac->addAction(QStringLiteral("project_open_for_file"), openProjectForFileAction); openProjectForFileAction->setText(i18n("Open Project for Current File")); connect( openProjectForFileAction, &QAction::triggered, this, &ProjectController::openProjectForUrlSlot); } ProjectController::~ProjectController() { delete d->model; delete d->dialog; } void ProjectController::cleanup() { if ( d->m_currentlyOpening.isEmpty() ) { d->saveListOfOpenedProjects(); } saveRecentProjectsActionEntries(); d->m_cleaningUp = true; if( buildSetModel() ) { buildSetModel()->storeToSession( Core::self()->activeSession() ); } closeAllProjects(); } void ProjectController::saveRecentProjectsActionEntries() { if (!d->m_recentProjectsAction) return; auto config = KSharedConfig::openConfig(); KConfigGroup recentGroup = config->group("RecentProjects"); d->m_recentProjectsAction->saveEntries( recentGroup ); config->sync(); } void ProjectController::initialize() { d->buildset = new ProjectBuildSetModel( this ); buildSetModel()->loadFromSession( Core::self()->activeSession() ); connect( this, &ProjectController::projectOpened, d->buildset, &ProjectBuildSetModel::loadFromProject ); connect( this, &ProjectController::projectClosing, d->buildset, &ProjectBuildSetModel::saveToProject ); connect( this, &ProjectController::projectClosed, d->buildset, &ProjectBuildSetModel::projectClosed ); d->m_changesModel = new ProjectChangesModel(this); loadSettings(false); d->dialog = new ProjectDialogProvider(d.data()); QDBusConnection::sessionBus().registerObject( QStringLiteral("/org/kdevelop/ProjectController"), this, QDBusConnection::ExportScriptableSlots ); KSharedConfigPtr config = Core::self()->activeSession()->config(); KConfigGroup group = config->group( "General Options" ); const auto projects = group.readEntry( "Open Projects", QList() ); connect( Core::self()->selectionController(), &ISelectionController::selectionChanged, this, [&] () { d->updateActionStates(); } ); connect(this, &ProjectController::projectOpened, this, [&] () { d->updateActionStates(); }); connect(this, &ProjectController::projectClosing, this, [&] () { d->updateActionStates(); }); QTimer::singleShot(0, this, [this, projects](){ openProjects(projects); emit initialized(); }); } void ProjectController::openProjects(const QList& projects) { for (const QUrl& url : projects) { openProject(url); } } void ProjectController::loadSettings( bool projectIsLoaded ) { Q_UNUSED(projectIsLoaded) } void ProjectController::saveSettings( bool projectIsLoaded ) { Q_UNUSED( projectIsLoaded ); } int ProjectController::projectCount() const { return d->m_projects.count(); } IProject* ProjectController::projectAt( int num ) const { if( !d->m_projects.isEmpty() && num >= 0 && num < d->m_projects.count() ) return d->m_projects.at( num ); return nullptr; } QList ProjectController::projects() const { return d->m_projects; } void ProjectController::eventuallyOpenProjectFile(KIO::Job* _job, const KIO::UDSEntryList& entries) { KIO::SimpleJob* job(dynamic_cast(_job)); Q_ASSERT(job); for (const KIO::UDSEntry& entry : entries) { if(d->m_foundProjectFile) break; if(!entry.isDir()) { QString name = entry.stringValue( KIO::UDSEntry::UDS_NAME ); if(name.endsWith(QLatin1String(".kdev4"))) { //We have found a project-file, open it openProject(Path(Path(job->url()), name).toUrl()); d->m_foundProjectFile = true; } } } } void ProjectController::openProjectForUrlSlot(bool) { if(ICore::self()->documentController()->activeDocument()) { QUrl url = ICore::self()->documentController()->activeDocument()->url(); IProject* project = ICore::self()->projectController()->findProjectForUrl(url); if(!project) { openProjectForUrl(url); }else{ KMessageBox::error(Core::self()->uiController()->activeMainWindow(), i18n("Project already open: %1", project->name())); } }else{ KMessageBox::error(Core::self()->uiController()->activeMainWindow(), i18n("No active document")); } } void ProjectController::openProjectForUrl(const QUrl& sourceUrl) { Q_ASSERT(!sourceUrl.isRelative()); QUrl dirUrl = sourceUrl; if (sourceUrl.isLocalFile() && !QFileInfo(sourceUrl.toLocalFile()).isDir()) { dirUrl = dirUrl.adjusted(QUrl::RemoveFilename); } QUrl testAt = dirUrl; d->m_foundProjectFile = false; while(!testAt.path().isEmpty()) { KIO::ListJob* job = KIO::listDir(testAt); connect(job, &KIO::ListJob::entries, this, &ProjectController::eventuallyOpenProjectFile); KJobWidgets::setWindow(job, ICore::self()->uiController()->activeMainWindow()); job->exec(); if(d->m_foundProjectFile) { //Fine! We have directly opened the project d->m_foundProjectFile = false; return; } QUrl oldTest = testAt.adjusted(QUrl::RemoveFilename); if(oldTest == testAt) break; } QUrl askForOpen = d->dialog->askProjectConfigLocation(false, dirUrl); if(askForOpen.isValid()) openProject(askForOpen); } void ProjectController::openProject( const QUrl &projectFile ) { QUrl url = projectFile; if ( url.isEmpty() ) { url = d->dialog->askProjectConfigLocation(false); if ( url.isEmpty() ) { return; } } Q_ASSERT(!url.isRelative()); QList existingSessions; if(!Core::self()->sessionController()->activeSession()->containedProjects().contains(url)) { foreach( const Session* session, Core::self()->sessionController()->sessions()) { if(session->containedProjects().contains(url)) { existingSessions << session; #if 0 ///@todo Think about this! Problem: The session might already contain files, the debugger might be active, etc. //If this session is empty, close it if(Core::self()->sessionController()->activeSession()->description().isEmpty()) { //Terminate this instance of kdevelop if the user agrees foreach(Sublime::MainWindow* window, Core::self()->uiController()->controller()->mainWindows()) window->close(); } #endif } } } if ( ! existingSessions.isEmpty() ) { ScopedDialog dialog(Core::self()->uiControllerInternal()->activeMainWindow()); dialog->setWindowTitle(i18n("Project Already Open")); auto mainLayout = new QVBoxLayout(dialog); mainLayout->addWidget(new QLabel(i18n("The project you're trying to open is already open in at least one " "other session.
What do you want to do?"))); QGroupBox sessions; sessions.setLayout(new QVBoxLayout); QRadioButton* newSession = new QRadioButton(i18n("Add project to current session")); sessions.layout()->addWidget(newSession); newSession->setChecked(true); foreach ( const Session* session, existingSessions ) { QRadioButton* button = new QRadioButton(i18n("Open session %1", session->description())); button->setProperty("sessionid", session->id().toString()); sessions.layout()->addWidget(button); } sessions.layout()->addItem(new QSpacerItem(0, 0, QSizePolicy::Expanding, QSizePolicy::Expanding)); mainLayout->addWidget(&sessions); auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Abort); auto okButton = buttonBox->button(QDialogButtonBox::Ok); okButton->setDefault(true); okButton->setShortcut(Qt::CTRL | Qt::Key_Return); connect(buttonBox, &QDialogButtonBox::accepted, dialog.data(), &QDialog::accept); connect(buttonBox, &QDialogButtonBox::rejected, dialog.data(), &QDialog::reject); mainLayout->addWidget(buttonBox); if (!dialog->exec()) return; foreach ( const QObject* obj, sessions.children() ) { if ( const QRadioButton* button = qobject_cast(obj) ) { QString sessionid = button->property("sessionid").toString(); if ( button->isChecked() && ! sessionid.isEmpty() ) { Core::self()->sessionController()->loadSession(sessionid); return; } } } } if ( url.isEmpty() ) { url = d->dialog->askProjectConfigLocation(false); } if ( !url.isEmpty() ) { d->importProject(url); } } bool ProjectController::fetchProjectFromUrl(const QUrl& repoUrl) { IPlugin* vcsOrProviderPlugin = nullptr; // TODO: query also projectprovider plugins, and that before plain vcs plugins // e.g. KDE provider plugin could catch URLs from mirror or pickup kde:repo things auto* pluginController = d->m_core->pluginController(); const auto& vcsPlugins = pluginController->allPluginsForExtension(QStringLiteral("org.kdevelop.IBasicVersionControl")); for (auto* plugin : vcsPlugins) { auto* iface = plugin->extension(); if (iface->isValidRemoteRepositoryUrl(repoUrl)) { vcsOrProviderPlugin = plugin; break; } } if (!vcsOrProviderPlugin) { KMessageBox::error(Core::self()->uiController()->activeMainWindow(), i18n("No enabled plugin supports this repository URL: %1", repoUrl.toDisplayString())); return false; } const QUrl url = d->dialog->askProjectConfigLocation(true, QUrl(), repoUrl, vcsOrProviderPlugin); if (!url.isEmpty()) { d->importProject(url); } return true; } void ProjectController::fetchProject() { QUrl url = d->dialog->askProjectConfigLocation(true); if ( !url.isEmpty() ) { d->importProject(url); } } void ProjectController::projectImportingFinished( IProject* project ) { if( !project ) { qCWarning(SHELL) << "OOOPS: 0-pointer project"; return; } IPlugin *managerPlugin = project->managerPlugin(); QList pluglist; pluglist.append( managerPlugin ); d->m_projectPlugins.insert( project, pluglist ); d->m_projects.append( project ); if ( d->m_currentlyOpening.isEmpty() ) { d->saveListOfOpenedProjects(); } if (Core::self()->setupFlags() != Core::NoUi) { d->m_recentProjectsAction->addUrl( project->projectFile().toUrl() ); saveRecentProjectsActionEntries(); } Q_ASSERT(d->m_currentlyOpening.contains(project->projectFile().toUrl())); d->m_currentlyOpening.removeAll(project->projectFile().toUrl()); emit projectOpened( project ); reparseProject(project); } // helper method for closeProject() void ProjectController::unloadUnusedProjectPlugins(IProject* proj) { QList pluginsForProj = d->m_projectPlugins.value( proj ); d->m_projectPlugins.remove( proj ); QList otherProjectPlugins; otherProjectPlugins.reserve(d->m_projectPlugins.size()); Q_FOREACH( const QList& _list, d->m_projectPlugins ) { otherProjectPlugins << _list; } QSet pluginsForProjSet = QSet::fromList( pluginsForProj ); QSet otherPrjPluginsSet = QSet::fromList( otherProjectPlugins ); // loaded - target = tobe unloaded. const QSet tobeRemoved = pluginsForProjSet.subtract( otherPrjPluginsSet ); for (IPlugin* _plugin : tobeRemoved) { KPluginMetaData _plugInfo = Core::self()->pluginController()->pluginInfo( _plugin ); if( _plugInfo.isValid() ) { QString _plugName = _plugInfo.pluginId(); qCDebug(SHELL) << "about to unloading :" << _plugName; Core::self()->pluginController()->unloadPlugin( _plugName ); } } } // helper method for closeProject() void ProjectController::closeAllOpenedFiles(IProject* proj) { foreach(IDocument* doc, Core::self()->documentController()->openDocuments()) { if (proj->inProject(IndexedString(doc->url()))) { doc->close(); } } } // helper method for closeProject() void ProjectController::initializePluginCleanup(IProject* proj) { // Unloading (and thus deleting) these plugins is not a good idea just yet // as we're being called by the view part and it gets deleted when we unload the plugin(s) // TODO: find a better place to unload connect(proj, &IProject::destroyed, this, [&] { d->unloadAllProjectPlugins(); }); } void ProjectController::takeProject(IProject* proj) { if (!proj) { return; } // loading might have failed d->m_currentlyOpening.removeAll(proj->projectFile().toUrl()); d->m_projects.removeAll(proj); emit projectClosing(proj); //Core::self()->saveSettings(); // The project file is being closed. // Now we can save settings for all of the Core // objects including this one!! unloadUnusedProjectPlugins(proj); closeAllOpenedFiles(proj); proj->close(); if (d->m_projects.isEmpty()) { initializePluginCleanup(proj); } if(!d->m_cleaningUp) d->saveListOfOpenedProjects(); emit projectClosed(proj); } void ProjectController::closeProject(IProject* proj) { takeProject(proj); proj->deleteLater(); // be safe when deleting } void ProjectController::closeAllProjects() { foreach (auto project, d->m_projects) { closeProject(project); } } void ProjectController::abortOpeningProject(IProject* proj) { d->m_currentlyOpening.removeAll(proj->projectFile().toUrl()); emit projectOpeningAborted(proj); } ProjectModel* ProjectController::projectModel() { return d->model; } IProject* ProjectController::findProjectForUrl( const QUrl& url ) const { if (d->m_projects.isEmpty()) { return nullptr; } ProjectBaseItem* item = d->model->itemForPath(IndexedString(url)); if (item) { return item->project(); } return nullptr; } IProject* ProjectController::findProjectByName( const QString& name ) { Q_FOREACH( IProject* proj, d->m_projects ) { if( proj->name() == name ) { return proj; } } return nullptr; } void ProjectController::configureProject( IProject* project ) { d->projectConfig( project ); } void ProjectController::addProject(IProject* project) { Q_ASSERT(project); if (d->m_projects.contains(project)) { qCWarning(SHELL) << "Project already tracked by this project controller:" << project; return; } // fake-emit signals so listeners are aware of a new project being added emit projectAboutToBeOpened(project); project->setParent(this); d->m_projects.append(project); emit projectOpened(project); } bool ProjectController::isProjectNameUsed( const QString& name ) const { foreach( IProject* p, projects() ) { if( p->name() == name ) { return true; } } return false; } QUrl ProjectController::projectsBaseDirectory() const { KConfigGroup group = ICore::self()->activeSession()->config()->group( "Project Manager" ); return group.readEntry("Projects Base Directory", QUrl::fromLocalFile(QDir::homePath() + QLatin1String("/projects"))); } QString ProjectController::prettyFilePath(const QUrl& url, FormattingOptions format) const { IProject* project = Core::self()->projectController()->findProjectForUrl(url); if(!project) { // Find a project with the correct base directory at least foreach(IProject* candidateProject, Core::self()->projectController()->projects()) { if(candidateProject->path().toUrl().isParentOf(url)) { project = candidateProject; break; } } } Path parent = Path(url).parent(); QString prefixText; if (project) { if (format == FormatHtml) { prefixText = QLatin1String("") + project->name() + QLatin1String("/"); } else { prefixText = project->name() + QLatin1Char(':'); } QString relativePath = project->path().relativePath(parent); if(relativePath.startsWith(QLatin1String("./"))) { relativePath.remove(0, 2); } if (!relativePath.isEmpty()) { prefixText += relativePath + QLatin1Char('/'); } } else { prefixText = parent.pathOrUrl() + QLatin1Char('/'); } return prefixText; } QString ProjectController::prettyFileName(const QUrl& url, FormattingOptions format) const { IProject* project = Core::self()->projectController()->findProjectForUrl(url); if(project && project->path() == Path(url)) { if (format == FormatHtml) { return QLatin1String("") + project->name() + QLatin1String(""); } else { return project->name(); } } QString prefixText = prettyFilePath( url, format ); if (format == FormatHtml) { return prefixText + QLatin1String("") + url.fileName() + QLatin1String(""); } else { return prefixText + url.fileName(); } } ContextMenuExtension ProjectController::contextMenuExtension(Context* ctx, QWidget* parent) { Q_UNUSED(parent); ContextMenuExtension ext; if ( ctx->type() != Context::ProjectItemContext || !static_cast(ctx)->items().isEmpty() ) { return ext; } ext.addAction(ContextMenuExtension::ProjectGroup, d->m_openProject); ext.addAction(ContextMenuExtension::ProjectGroup, d->m_fetchProject); ext.addAction(ContextMenuExtension::ProjectGroup, d->m_recentProjectsAction); return ext; } ProjectBuildSetModel* ProjectController::buildSetModel() { return d->buildset; } ProjectChangesModel* ProjectController::changesModel() { return d->m_changesModel; } void ProjectController::commitCurrentProject() { IDocument* doc=ICore::self()->documentController()->activeDocument(); if(!doc) return; QUrl url=doc->url(); IProject* project = ICore::self()->projectController()->findProjectForUrl(url); if(project && project->versionControlPlugin()) { IPlugin* plugin = project->versionControlPlugin(); IBasicVersionControl* vcs=plugin->extension(); if(vcs) { ICore::self()->documentController()->saveAllDocuments(KDevelop::IDocument::Silent); const Path basePath = project->path(); VCSCommitDiffPatchSource* patchSource = new VCSCommitDiffPatchSource(new VCSStandardDiffUpdater(vcs, basePath.toUrl())); bool ret = showVcsDiff(patchSource); if(!ret) { ScopedDialog commitDialog(patchSource); commitDialog->setCommitCandidates(patchSource->infos()); commitDialog->exec(); } } } } QString ProjectController::mapSourceBuild( const QString& path_, bool reverse, bool fallbackRoot ) const { Path path(path_); IProject* sourceDirProject = nullptr, *buildDirProject = nullptr; Q_FOREACH(IProject* proj, d->m_projects) { if(proj->path().isParentOf(path) || proj->path() == path) sourceDirProject = proj; if(proj->buildSystemManager()) { Path buildDir = proj->buildSystemManager()->buildDirectory(proj->projectItem()); if(buildDir.isValid() && (buildDir.isParentOf(path) || buildDir == path)) buildDirProject = proj; } } if(!reverse) { // Map-target is the build directory if(sourceDirProject && sourceDirProject->buildSystemManager()) { // We're in the source, map into the build directory QString relativePath = sourceDirProject->path().relativePath(path); Path build = sourceDirProject->buildSystemManager()->buildDirectory(sourceDirProject->projectItem()); build.addPath(relativePath); while(!QFile::exists(build.path())) build = build.parent(); return build.pathOrUrl(); }else if(buildDirProject && fallbackRoot) { // We're in the build directory, map to the build directory root return buildDirProject->buildSystemManager()->buildDirectory(buildDirProject->projectItem()).pathOrUrl(); } }else{ // Map-target is the source directory if(buildDirProject) { Path build = buildDirProject->buildSystemManager()->buildDirectory(buildDirProject->projectItem()); // We're in the source, map into the build directory QString relativePath = build.relativePath(path); Path source = buildDirProject->path(); source.addPath(relativePath); while(!QFile::exists(source.path())) source = source.parent(); return source.pathOrUrl(); }else if(sourceDirProject && fallbackRoot) { // We're in the source directory, map to the root return sourceDirProject->path().pathOrUrl(); } } return QString(); } void ProjectController::reparseProject( IProject* project, bool forceUpdate ) { if (auto job = d->m_parseJobs.value(project)) { job->kill(); } d->m_parseJobs[project] = new KDevelop::ParseProjectJob(project, forceUpdate); ICore::self()->runController()->registerJob(d->m_parseJobs[project]); } } diff --git a/plugins/classbrowser/classwidget.cpp b/plugins/classbrowser/classwidget.cpp index abfca34aeb..a278ec8b3e 100644 --- a/plugins/classbrowser/classwidget.cpp +++ b/plugins/classbrowser/classwidget.cpp @@ -1,111 +1,111 @@ /* * KDevelop Class viewer * * Copyright 2006 Adam Treat * Copyright (c) 2006-2007 Hamish Rodda * Copyright 2009 Lior Mualem * * This program 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 program 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 General Public License for more details. * * You should have received a copy of the GNU General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "classwidget.h" #include #include #include #include #include #include #include #include "language/classmodel/classmodel.h" #include "classtree.h" #include "classbrowserplugin.h" using namespace KDevelop; ClassWidget::ClassWidget(QWidget* parent, ClassBrowserPlugin* plugin) : QWidget(parent) , m_plugin(plugin) , m_model(new ClassModel()) , m_tree(new ClassTree(this, plugin)) , m_searchLine(new QLineEdit(this)) , m_filterTimer(new QTimer(this)) { setObjectName(QStringLiteral("Class Browser Tree")); setWindowTitle(i18n("Classes")); setWindowIcon(QIcon::fromTheme(QStringLiteral("code-class"), windowIcon())); // Set tree in the plugin m_plugin->setActiveClassTree(m_tree); // Set model in the tree view m_tree->setModel(m_model); m_tree->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents); m_tree->header()->setStretchLastSection(false); // We need notification in the model for the collapse/expansion of nodes. connect(m_tree, &ClassTree::collapsed, m_model, &ClassModel::collapsed); connect(m_tree, &ClassTree::expanded, m_model, &ClassModel::expanded); // Init filter timer m_filterTimer->setSingleShot(true); - connect(m_filterTimer, &QTimer::timeout, [this]() { + connect(m_filterTimer, &QTimer::timeout, this, [this]() { m_model->updateFilterString(m_filterText); if (m_filterText.isEmpty()) m_tree->collapseAll(); else m_tree->expandToDepth(0); }); // Init search box m_searchLine->setClearButtonEnabled( true ); - connect(m_searchLine, &QLineEdit::textChanged, [this](const QString& newFilter) { + connect(m_searchLine, &QLineEdit::textChanged, this, [this](const QString& newFilter) { m_filterText = newFilter; m_filterTimer->start(500); }); QLabel *searchLabel = new QLabel( i18n("S&earch:"), this ); searchLabel->setBuddy( m_searchLine ); QHBoxLayout* layout = new QHBoxLayout(); layout->setSpacing( 5 ); layout->setMargin( 0 ); layout->addWidget(searchLabel); layout->addWidget(m_searchLine); setFocusProxy( m_searchLine ); QVBoxLayout* vbox = new QVBoxLayout(this); vbox->setMargin(0); vbox->addLayout(layout); vbox->addWidget(m_tree); setLayout( vbox ); setWhatsThis( i18n( "Class Browser" ) ); } ClassWidget::~ClassWidget() { delete m_model; } // kate: space-indent on; indent-width 2; tab-width: 4; replace-tabs on; auto-insert-doxygen on diff --git a/plugins/custom-definesandincludes/noprojectincludesanddefines/noprojectincludepathsmanager.cpp b/plugins/custom-definesandincludes/noprojectincludesanddefines/noprojectincludepathsmanager.cpp index e7cdbd6948..d575f77c89 100644 --- a/plugins/custom-definesandincludes/noprojectincludesanddefines/noprojectincludepathsmanager.cpp +++ b/plugins/custom-definesandincludes/noprojectincludesanddefines/noprojectincludepathsmanager.cpp @@ -1,156 +1,156 @@ /* * This file is part of KDevelop * * Copyright 2014 Sergey Kalinichev * * This program 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 program 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 General Public License for more details. * * You should have received a copy of the GNU General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "noprojectincludepathsmanager.h" #include #include #include #include #include #include #include #include #include "noprojectcustomincludepaths.h" namespace { const QString includePathsFile = QStringLiteral(".kdev_include_paths"); bool removeSettings(const QString& storageDirectory) { QString file = storageDirectory + QDir::separator() + includePathsFile; return QFile::remove(file); } QStringList pathListToStringList(const Path::List& paths) { QStringList sl; sl.reserve(paths.size()); for (const auto& p : paths) { sl << p.path(); } return sl; } } QString NoProjectIncludePathsManager::findConfigurationFile(const QString& path) { QDir dir(path); while (dir.exists()) { QFileInfo customIncludePathsFile(dir, includePathsFile); if (customIncludePathsFile.exists()) { return customIncludePathsFile.absoluteFilePath(); } if (!dir.cdUp()) { break; } } return {}; } std::pair> NoProjectIncludePathsManager::includesAndDefines(const QString& path) { QFileInfo fi(path); auto pathToFile = findConfigurationFile(fi.absoluteDir().absolutePath()); if (pathToFile.isEmpty()) { return {}; } Path::List includes; QHash defines; QFile f(pathToFile); if (f.open(QIODevice::ReadOnly | QIODevice::Text)) { auto lines = QString::fromLocal8Bit(f.readAll()).split('\n', QString::SkipEmptyParts); QFileInfo dir(pathToFile); for (const auto& line : lines) { auto textLine = line.trimmed(); if (textLine.startsWith(QLatin1String("#define "))) { QStringList items = textLine.split(' '); if (items.length() > 1) { defines[items[1]] = QStringList(items.mid(2)).join(' '); }else{ qWarning() << i18n("Bad #define directive in %1: %2", pathToFile, textLine); } continue; } if (!textLine.isEmpty()) { QFileInfo pathInfo(textLine); if (pathInfo.isRelative()) { includes << Path(dir.canonicalPath() + QDir::separator() + textLine); } else { includes << Path(textLine); } } } f.close(); } return std::make_pair(includes, defines); } bool NoProjectIncludePathsManager::writeIncludePaths(const QString& storageDirectory, const QStringList& includePaths) { QDir dir(storageDirectory); QFileInfo customIncludePaths(dir, includePathsFile); QFile f(customIncludePaths.filePath()); if (f.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) { QTextStream out(&f); for (const auto& customPath : includePaths) { out << customPath << endl; } if (includePaths.isEmpty()) { removeSettings(storageDirectory); } return true; } else { return false; } } void NoProjectIncludePathsManager::openConfigurationDialog(const QString& path) { auto cip = new NoProjectCustomIncludePaths; cip->setAttribute(Qt::WA_DeleteOnClose); cip->setModal(true); QFileInfo fi(path); auto dir = fi.absoluteDir().absolutePath(); cip->setStorageDirectory(dir); auto paths = includesAndDefines(path).first; cip->setCustomIncludePaths(pathListToStringList(paths)); - QObject::connect(cip, &QDialog::accepted, [this, cip, &path]() { + QObject::connect(cip, &QDialog::accepted, cip, [this, cip, &path]() { if (!writeIncludePaths(cip->storageDirectory(), cip->customIncludePaths())) { qWarning() << i18n("Failed to save custom include paths in directory: %1", cip->storageDirectory()); } KDevelop::ICore::self()->languageController()->backgroundParser()->addDocument(KDevelop::IndexedString(path)); }); } diff --git a/plugins/debuggercommon/midebugsession.cpp b/plugins/debuggercommon/midebugsession.cpp index 11a1520102..5c8013b030 100644 --- a/plugins/debuggercommon/midebugsession.cpp +++ b/plugins/debuggercommon/midebugsession.cpp @@ -1,1309 +1,1304 @@ /* * Common code for debugger support * * Copyright 1999-2001 John Birch * Copyright 2001 by Bernd Gehrmann * Copyright 2006 Vladimir Prus * Copyright 2007 Hamish Rodda * Copyright 2009 Niko Sams * Copyright 2016 Aetf * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #include "midebugsession.h" #include "debuglog.h" #include "midebugger.h" #include "mivariable.h" #include "mi/mi.h" #include "mi/micommand.h" #include "mi/micommandqueue.h" #include "stty.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include -#include #include #include #include using namespace KDevelop; using namespace KDevMI; using namespace KDevMI::MI; MIDebugSession::MIDebugSession(MIDebuggerPlugin *plugin) : m_procLineMaker(new ProcessLineMaker(this)) , m_commandQueue(new CommandQueue) , m_debuggerState(s_dbgNotStarted | s_appNotStarted) , m_tty(nullptr) , m_plugin(plugin) { // setup signals connect(m_procLineMaker, &ProcessLineMaker::receivedStdoutLines, this, &MIDebugSession::inferiorStdoutLines); connect(m_procLineMaker, &ProcessLineMaker::receivedStderrLines, this, &MIDebugSession::inferiorStderrLines); // forward tty output to process line maker connect(this, &MIDebugSession::inferiorTtyStdout, m_procLineMaker, &ProcessLineMaker::slotReceivedStdout); connect(this, &MIDebugSession::inferiorTtyStderr, m_procLineMaker, &ProcessLineMaker::slotReceivedStderr); // FIXME: see if this still works //connect(statusBarIndicator, SIGNAL(doubleClicked()), // controller, SLOT(explainDebuggerStatus())); // FIXME: reimplement / re-enable //connect(this, SIGNAL(addWatchVariable(QString)), controller->variables(), SLOT(slotAddWatchVariable(QString))); //connect(this, SIGNAL(evaluateExpression(QString)), controller->variables(), SLOT(slotEvaluateExpression(QString))); } MIDebugSession::~MIDebugSession() { qCDebug(DEBUGGERCOMMON) << "Destroying MIDebugSession"; // Deleting the session involves shutting down gdb nicely. // When were attached to a process, we must first detach so that the process // can continue running as it was before being attached. gdb is quite slow to // detach from a process, so we must process events within here to get a "clean" // shutdown. if (!debuggerStateIsOn(s_dbgNotStarted)) { stopDebugger(); } } IDebugSession::DebuggerState MIDebugSession::state() const { return m_sessionState; } QMap & MIDebugSession::variableMapping() { return m_allVariables; } MIVariable* MIDebugSession::findVariableByVarobjName(const QString &varobjName) const { if (m_allVariables.count(varobjName) == 0) return nullptr; return m_allVariables.value(varobjName); } void MIDebugSession::markAllVariableDead() { for (auto i = m_allVariables.begin(), e = m_allVariables.end(); i != e; ++i) { i.value()->markAsDead(); } m_allVariables.clear(); } bool MIDebugSession::restartAvaliable() const { if (debuggerStateIsOn(s_attached) || debuggerStateIsOn(s_core)) { return false; } else { return true; } } bool MIDebugSession::startDebugger(ILaunchConfiguration *cfg) { qCDebug(DEBUGGERCOMMON) << "Starting new debugger instance"; if (m_debugger) { qCWarning(DEBUGGERCOMMON) << "m_debugger object still exists"; delete m_debugger; m_debugger = nullptr; } m_debugger = createDebugger(); m_debugger->setParent(this); // output signals connect(m_debugger, &MIDebugger::applicationOutput, this, [this](const QString &output) { auto lines = output.split(QRegularExpression(QStringLiteral("[\r\n]")), QString::SkipEmptyParts); for (auto &line : lines) { int p = line.length(); while (p >= 1 && (line[p-1] == '\r' || line[p-1] == '\n')) p--; if (p != line.length()) line.truncate(p); } emit inferiorStdoutLines(lines); }); connect(m_debugger, &MIDebugger::userCommandOutput, this, &MIDebugSession::debuggerUserCommandOutput); connect(m_debugger, &MIDebugger::internalCommandOutput, this, &MIDebugSession::debuggerInternalCommandOutput); connect(m_debugger, &MIDebugger::debuggerInternalOutput, this, &MIDebugSession::debuggerInternalOutput); // state signals connect(m_debugger, &MIDebugger::programStopped, this, &MIDebugSession::inferiorStopped); connect(m_debugger, &MIDebugger::programRunning, this, &MIDebugSession::inferiorRunning); // internal handlers connect(m_debugger, &MIDebugger::ready, this, &MIDebugSession::slotDebuggerReady); connect(m_debugger, &MIDebugger::exited, this, &MIDebugSession::slotDebuggerExited); connect(m_debugger, &MIDebugger::programStopped, this, &MIDebugSession::slotInferiorStopped); connect(m_debugger, &MIDebugger::programRunning, this, &MIDebugSession::slotInferiorRunning); connect(m_debugger, &MIDebugger::notification, this, &MIDebugSession::processNotification); // start the debugger. Do this after connecting all signals so that initial // debugger output, and important events like the debugger died are reported. QStringList extraArguments; if (!m_sourceInitFile) extraArguments << QStringLiteral("--nx"); auto config = cfg ? cfg->config() // FIXME: this is only used when attachToProcess or examineCoreFile. // Change to use a global launch configuration when calling : KConfigGroup(KSharedConfig::openConfig(), "GDB Config"); if (!m_debugger->start(config, extraArguments)) { // debugger failed to start, ensure debugger and session state are correctly updated. setDebuggerStateOn(s_dbgFailedStart); return false; } // FIXME: here, we should wait until the debugger is up and waiting for input. // Then, clear s_dbgNotStarted // It's better to do this right away so that the state bit is always correct. setDebuggerStateOff(s_dbgNotStarted); // Initialise debugger. At this stage debugger is sitting wondering what to do, // and to whom. initializeDebugger(); qCDebug(DEBUGGERCOMMON) << "Debugger instance started"; return true; } bool MIDebugSession::startDebugging(ILaunchConfiguration* cfg, IExecutePlugin* iexec) { qCDebug(DEBUGGERCOMMON) << "Starting new debug session"; Q_ASSERT(cfg); Q_ASSERT(iexec); // Ensure debugger is started first if (debuggerStateIsOn(s_appNotStarted)) { emit showMessage(i18n("Running program"), 1000); } if (debuggerStateIsOn(s_dbgNotStarted)) { if (!startDebugger(cfg)) return false; } if (debuggerStateIsOn(s_shuttingDown)) { qCDebug(DEBUGGERCOMMON) << "Tried to run when debugger shutting down"; return false; } // Only dummy err here, actual erros have been checked already in the job and we don't get here if there were any QString err; QString executable = iexec->executable(cfg, err).toLocalFile(); configInferior(cfg, iexec, executable); // Set up the tty for the inferior bool config_useExternalTerminal = iexec->useTerminal(cfg); QString config_ternimalName = iexec->terminal(cfg); if (!config_ternimalName.isEmpty()) { // the external terminal cmd contains additional arguments, just get the terminal name config_ternimalName = KShell::splitArgs(config_ternimalName).first(); } m_tty.reset(new STTY(config_useExternalTerminal, config_ternimalName)); if (!config_useExternalTerminal) { connect(m_tty.get(), &STTY::OutOutput, this, &MIDebugSession::inferiorTtyStdout); connect(m_tty.get(), &STTY::ErrOutput, this, &MIDebugSession::inferiorTtyStderr); } QString tty(m_tty->getSlave()); #ifndef Q_OS_WIN if (tty.isEmpty()) { KMessageBox::information(qApp->activeWindow(), m_tty->lastError(), i18n("warning")); m_tty.reset(nullptr); return false; } #endif addCommand(InferiorTtySet, tty); // Change the working directory to the correct one QString dir = iexec->workingDirectory(cfg).toLocalFile(); if (dir.isEmpty()) { dir = QFileInfo(executable).absolutePath(); } addCommand(EnvironmentCd, '"' + dir + '"'); // Set the run arguments QStringList arguments = iexec->arguments(cfg, err); if (!arguments.isEmpty()) addCommand(ExecArguments, KShell::joinArgs(arguments)); // Do other debugger specific config options and actually start the inferior program if (!execInferior(cfg, iexec, executable)) { return false; } QString config_startWith = cfg->config().readEntry(Config::StartWithEntry, QStringLiteral("ApplicationOutput")); if (config_startWith == QLatin1String("GdbConsole")) { emit raiseDebuggerConsoleViews(); } else if (config_startWith == QLatin1String("FrameStack")) { emit raiseFramestackViews(); } else { // ApplicationOutput is raised in DebugJob (by setting job to Verbose/Silent) } return true; } // FIXME: use same configuration process as startDebugging bool MIDebugSession::attachToProcess(int pid) { qCDebug(DEBUGGERCOMMON) << "Attach to process" << pid; emit showMessage(i18n("Attaching to process %1", pid), 1000); if (debuggerStateIsOn(s_dbgNotStarted)) { // FIXME: use global launch configuration rather than nullptr if (!startDebugger(nullptr)) { return false; } } setDebuggerStateOn(s_attached); //set current state to running, after attaching we will get *stopped response setDebuggerStateOn(s_appRunning); addCommand(TargetAttach, QString::number(pid), this, &MIDebugSession::handleTargetAttach, CmdHandlesError); addCommand(new SentinelCommand(breakpointController(), &MIBreakpointController::initSendBreakpoints)); raiseEvent(connected_to_program); emit raiseFramestackViews(); return true; } void MIDebugSession::handleTargetAttach(const MI::ResultRecord& r) { if (r.reason == QLatin1String("error")) { KMessageBox::error( qApp->activeWindow(), i18n("Could not attach debugger:
")+ r[QStringLiteral("msg")].literal(), i18n("Startup error")); stopDebugger(); } } bool MIDebugSession::examineCoreFile(const QUrl &debugee, const QUrl &coreFile) { emit showMessage(i18n("Examining core file %1", coreFile.toLocalFile()), 1000); if (debuggerStateIsOn(s_dbgNotStarted)) { // FIXME: use global launch configuration rather than nullptr if (!startDebugger(nullptr)) { return false; } } // FIXME: support non-local URLs if (!loadCoreFile(nullptr, debugee.toLocalFile(), coreFile.toLocalFile())) { return false; } raiseEvent(program_state_changed); return true; } #define ENUM_NAME(o,e,v) (o::staticMetaObject.enumerator(o::staticMetaObject.indexOfEnumerator(#e)).valueToKey((v))) void MIDebugSession::setSessionState(DebuggerState state) { qCDebug(DEBUGGERCOMMON) << "Session state changed to" << ENUM_NAME(IDebugSession, DebuggerState, state) << "(" << state << ")"; if (state != m_sessionState) { m_sessionState = state; emit stateChanged(state); } } bool MIDebugSession::debuggerStateIsOn(DBGStateFlags state) const { return m_debuggerState & state; } DBGStateFlags MIDebugSession::debuggerState() const { return m_debuggerState; } void MIDebugSession::setDebuggerStateOn(DBGStateFlags stateOn) { DBGStateFlags oldState = m_debuggerState; debuggerStateChange(m_debuggerState, m_debuggerState | stateOn); m_debuggerState |= stateOn; handleDebuggerStateChange(oldState, m_debuggerState); } void MIDebugSession::setDebuggerStateOff(DBGStateFlags stateOff) { DBGStateFlags oldState = m_debuggerState; debuggerStateChange(m_debuggerState, m_debuggerState & ~stateOff); m_debuggerState &= ~stateOff; handleDebuggerStateChange(oldState, m_debuggerState); } void MIDebugSession::setDebuggerState(DBGStateFlags newState) { DBGStateFlags oldState = m_debuggerState; debuggerStateChange(m_debuggerState, newState); m_debuggerState = newState; handleDebuggerStateChange(oldState, m_debuggerState); } void MIDebugSession::debuggerStateChange(DBGStateFlags oldState, DBGStateFlags newState) { int delta = oldState ^ newState; if (delta) { QString out; #define STATE_CHECK(name) \ do { \ if (delta & name) { \ out += ((newState & name) ? " +" : " -"); \ out += #name; \ delta &= ~name; \ } \ } while (0) STATE_CHECK(s_dbgNotStarted); STATE_CHECK(s_appNotStarted); STATE_CHECK(s_programExited); STATE_CHECK(s_attached); STATE_CHECK(s_core); STATE_CHECK(s_shuttingDown); STATE_CHECK(s_dbgBusy); STATE_CHECK(s_appRunning); STATE_CHECK(s_dbgNotListening); STATE_CHECK(s_automaticContinue); #undef STATE_CHECK for (unsigned int i = 0; delta != 0 && i < 32; ++i) { if (delta & (1 << i)) { delta &= ~(1 << i); out += (((1 << i) & newState) ? " +" : " -") + QString::number(i); } } } } void MIDebugSession::handleDebuggerStateChange(DBGStateFlags oldState, DBGStateFlags newState) { QString message; DebuggerState oldSessionState = state(); DebuggerState newSessionState = oldSessionState; DBGStateFlags changedState = oldState ^ newState; if (newState & s_dbgNotStarted) { if (changedState & s_dbgNotStarted) { message = i18n("Debugger stopped"); emit finished(); } if (oldSessionState != NotStartedState || newState & s_dbgFailedStart) { newSessionState = EndedState; } } else { if (newState & s_appNotStarted) { if (oldSessionState == NotStartedState || oldSessionState == StartingState) { newSessionState = StartingState; } else { newSessionState = StoppedState; } } else if (newState & s_programExited) { if (changedState & s_programExited) { message = i18n("Process exited"); } newSessionState = StoppedState; } else if (newState & s_appRunning) { if (changedState & s_appRunning) { message = i18n("Application is running"); } newSessionState = ActiveState; } else { if (changedState & s_appRunning) { message = i18n("Application is paused"); } newSessionState = PausedState; } } // And now? :-) qCDebug(DEBUGGERCOMMON) << "Debugger state changed to:" << newState << message << "- changes:" << changedState; if (!message.isEmpty()) emit showMessage(message, 3000); emit debuggerStateChanged(oldState, newState); // must be last, since it can lead to deletion of the DebugSession if (newSessionState != oldSessionState) { setSessionState(newSessionState); } } void MIDebugSession::restartDebugger() { // We implement restart as kill + slotRun, as opposed as plain "run" // command because kill + slotRun allows any special logic in slotRun // to apply for restart. // // That includes: // - checking for out-of-date project // - special setup for remote debugging. // // Had we used plain 'run' command, restart for remote debugging simply // would not work. if (!debuggerStateIsOn(s_dbgNotStarted|s_shuttingDown)) { // FIXME: s_dbgBusy or m_debugger->isReady()? if (debuggerStateIsOn(s_dbgBusy)) { interruptDebugger(); } // The -exec-abort is not implemented in gdb // addCommand(ExecAbort); addCommand(NonMI, QStringLiteral("kill")); } run(); } void MIDebugSession::stopDebugger() { if (debuggerStateIsOn(s_dbgNotStarted)) { // we are force to stop even before debugger started, just reset qCDebug(DEBUGGERCOMMON) << "Stopping debugger when it's not started"; return; } m_commandQueue->clear(); qCDebug(DEBUGGERCOMMON) << "try stopping debugger"; if (debuggerStateIsOn(s_shuttingDown) || !m_debugger) return; setDebuggerStateOn(s_shuttingDown); qCDebug(DEBUGGERCOMMON) << "stopping debugger"; // Get debugger's attention if it's busy. We need debugger to be at the // command line so we can stop it. if (!m_debugger->isReady()) { qCDebug(DEBUGGERCOMMON) << "debugger busy on shutdown - interrupting"; interruptDebugger(); } // If the app is attached then we release it here. This doesn't stop // the app running. if (debuggerStateIsOn(s_attached)) { addCommand(TargetDetach); emit debuggerUserCommandOutput(QStringLiteral("(gdb) detach\n")); } // Now try to stop debugger running. addCommand(GdbExit); emit debuggerUserCommandOutput(QStringLiteral("(gdb) quit")); // We cannot wait forever, kill gdb after 5 seconds if it's not yet quit - QPointer guarded_this(this); - QTimer::singleShot(5000, [guarded_this](){ - if (guarded_this) { - if (!guarded_this->debuggerStateIsOn(s_programExited) - && guarded_this->debuggerStateIsOn(s_shuttingDown)) { - qCDebug(DEBUGGERCOMMON) << "debugger not shutdown - killing"; - guarded_this->m_debugger->kill(); - guarded_this->setDebuggerState(s_dbgNotStarted | s_appNotStarted); - guarded_this->raiseEvent(debugger_exited); - } + QTimer::singleShot(5000, this, [this]() { + if (!debuggerStateIsOn(s_programExited) && debuggerStateIsOn(s_shuttingDown)) { + qCDebug(DEBUGGERCOMMON) << "debugger not shutdown - killing"; + m_debugger->kill(); + setDebuggerState(s_dbgNotStarted | s_appNotStarted); + raiseEvent(debugger_exited); } }); emit reset(); } void MIDebugSession::interruptDebugger() { Q_ASSERT(m_debugger); // Explicitly send the interrupt in case something went wrong with the usual // ensureGdbListening logic. m_debugger->interrupt(); addCommand(ExecInterrupt, QString(), CmdInterrupt); } void MIDebugSession::run() { if (debuggerStateIsOn(s_appNotStarted|s_dbgNotStarted|s_shuttingDown)) return; addCommand(MI::ExecContinue, QString(), CmdMaybeStartsRunning); } void MIDebugSession::runToCursor() { if (IDocument* doc = ICore::self()->documentController()->activeDocument()) { KTextEditor::Cursor cursor = doc->cursorPosition(); if (cursor.isValid()) runUntil(doc->url(), cursor.line() + 1); } } void MIDebugSession::jumpToCursor() { if (IDocument* doc = ICore::self()->documentController()->activeDocument()) { KTextEditor::Cursor cursor = doc->cursorPosition(); if (cursor.isValid()) jumpTo(doc->url(), cursor.line() + 1); } } void MIDebugSession::stepOver() { if (debuggerStateIsOn(s_appNotStarted|s_shuttingDown)) return; addCommand(ExecNext, QString(), CmdMaybeStartsRunning | CmdTemporaryRun); } void MIDebugSession::stepIntoInstruction() { if (debuggerStateIsOn(s_appNotStarted|s_shuttingDown)) return; addCommand(ExecStepInstruction, QString(), CmdMaybeStartsRunning | CmdTemporaryRun); } void MIDebugSession::stepInto() { if (debuggerStateIsOn(s_appNotStarted|s_shuttingDown)) return; addCommand(ExecStep, QString(), CmdMaybeStartsRunning | CmdTemporaryRun); } void MIDebugSession::stepOverInstruction() { if (debuggerStateIsOn(s_appNotStarted|s_shuttingDown)) return; addCommand(ExecNextInstruction, QString(), CmdMaybeStartsRunning | CmdTemporaryRun); } void MIDebugSession::stepOut() { if (debuggerStateIsOn(s_appNotStarted|s_shuttingDown)) return; addCommand(ExecFinish, QString(), CmdMaybeStartsRunning | CmdTemporaryRun); } void MIDebugSession::runUntil(const QUrl& url, int line) { if (debuggerStateIsOn(s_dbgNotStarted|s_shuttingDown)) return; if (!url.isValid()) { addCommand(ExecUntil, QString::number(line), CmdMaybeStartsRunning | CmdTemporaryRun); } else { addCommand(ExecUntil, QStringLiteral("%1:%2").arg(url.toLocalFile()).arg(line), CmdMaybeStartsRunning | CmdTemporaryRun); } } void MIDebugSession::runUntil(const QString& address) { if (debuggerStateIsOn(s_dbgNotStarted|s_shuttingDown)) return; if (!address.isEmpty()) { addCommand(ExecUntil, QStringLiteral("*%1").arg(address), CmdMaybeStartsRunning | CmdTemporaryRun); } } void MIDebugSession::jumpTo(const QUrl& url, int line) { if (debuggerStateIsOn(s_dbgNotStarted|s_shuttingDown)) return; if (url.isValid()) { addCommand(NonMI, QStringLiteral("tbreak %1:%2").arg(url.toLocalFile()).arg(line)); addCommand(NonMI, QStringLiteral("jump %1:%2").arg(url.toLocalFile()).arg(line)); } } void MIDebugSession::jumpToMemoryAddress(const QString& address) { if (debuggerStateIsOn(s_dbgNotStarted|s_shuttingDown)) return; if (!address.isEmpty()) { addCommand(NonMI, QStringLiteral("tbreak *%1").arg(address)); addCommand(NonMI, QStringLiteral("jump *%1").arg(address)); } } void MIDebugSession::addUserCommand(const QString& cmd) { auto usercmd = createUserCommand(cmd); if (!usercmd) return; queueCmd(usercmd); // User command can theoreticall modify absolutely everything, // so need to force a reload. // We can do it right now, and don't wait for user command to finish // since commands used to reload all view will be executed after // user command anyway. if (!debuggerStateIsOn(s_appNotStarted) && !debuggerStateIsOn(s_programExited)) raiseEvent(program_state_changed); } MICommand *MIDebugSession::createUserCommand(const QString &cmd) const { MICommand *res = nullptr; if (!cmd.isEmpty() && cmd[0].isDigit()) { // Add a space to the beginning, so debugger won't get confused if the // command starts with a number (won't mix it up with command token added) res = new UserCommand(MI::NonMI, QLatin1Char(' ') + cmd); } else { res = new UserCommand(MI::NonMI, cmd); } return res; } MICommand *MIDebugSession::createCommand(CommandType type, const QString& arguments, CommandFlags flags) const { return new MICommand(type, arguments, flags); } void MIDebugSession::addCommand(MICommand* cmd) { queueCmd(cmd); } void MIDebugSession::addCommand(MI::CommandType type, const QString& arguments, MI::CommandFlags flags) { queueCmd(createCommand(type, arguments, flags)); } void MIDebugSession::addCommand(MI::CommandType type, const QString& arguments, MI::MICommandHandler *handler, MI::CommandFlags flags) { auto cmd = createCommand(type, arguments, flags); cmd->setHandler(handler); queueCmd(cmd); } void MIDebugSession::addCommand(MI::CommandType type, const QString& arguments, const MI::FunctionCommandHandler::Function& callback, MI::CommandFlags flags) { auto cmd = createCommand(type, arguments, flags); cmd->setHandler(callback); queueCmd(cmd); } // Fairly obvious that we'll add whatever command you give me to a queue // Not quite so obvious though is that if we are going to run again. then any // information requests become redundent and must be removed. // We also try and run whatever command happens to be at the head of // the queue. void MIDebugSession::queueCmd(MICommand *cmd) { if (debuggerStateIsOn(s_dbgNotStarted)) { KMessageBox::information( qApp->activeWindow(), i18n("Gdb command sent when debugger is not running
" "The command was:
%1", cmd->initialString()), i18n("Internal error")); return; } if (m_stateReloadInProgress) cmd->setStateReloading(true); m_commandQueue->enqueue(cmd); qCDebug(DEBUGGERCOMMON) << "QUEUE: " << cmd->initialString() << (m_stateReloadInProgress ? "(state reloading)" : "") << m_commandQueue->count() << "pending"; bool varCommandWithContext= (cmd->type() >= MI::VarAssign && cmd->type() <= MI::VarUpdate && cmd->type() != MI::VarDelete); bool stackCommandWithContext = (cmd->type() >= MI::StackInfoDepth && cmd->type() <= MI::StackListLocals); if (varCommandWithContext || stackCommandWithContext) { if (cmd->thread() == -1) qCDebug(DEBUGGERCOMMON) << "\t--thread will be added on execution"; if (cmd->frame() == -1) qCDebug(DEBUGGERCOMMON) << "\t--frame will be added on execution"; } setDebuggerStateOn(s_dbgBusy); raiseEvent(debugger_busy); executeCmd(); } void MIDebugSession::executeCmd() { Q_ASSERT(m_debugger); if (debuggerStateIsOn(s_dbgNotListening) && m_commandQueue->haveImmediateCommand()) { // We may have to call this even while a command is currently executing, because // debugger can get into a state where a command such as ExecRun does not send a response // while the inferior is running. ensureDebuggerListening(); } if (!m_debugger->isReady()) return; MICommand* currentCmd = m_commandQueue->nextCommand(); if (!currentCmd) return; if (currentCmd->flags() & (CmdMaybeStartsRunning | CmdInterrupt)) { setDebuggerStateOff(s_automaticContinue); } if (currentCmd->flags() & CmdMaybeStartsRunning) { // GDB can be in a state where it is listening for commands while the program is running. // However, when we send a command such as ExecContinue in this state, GDB may return to // the non-listening state without acknowledging that the ExecContinue command has even // finished, let alone sending a new notification about the program's running state. // So let's be extra cautious about ensuring that we will wake GDB up again if required. setDebuggerStateOn(s_dbgNotListening); } bool varCommandWithContext= (currentCmd->type() >= MI::VarAssign && currentCmd->type() <= MI::VarUpdate && currentCmd->type() != MI::VarDelete); bool stackCommandWithContext = (currentCmd->type() >= MI::StackInfoDepth && currentCmd->type() <= MI::StackListLocals); if (varCommandWithContext || stackCommandWithContext) { // Most var commands should be executed in the context // of the selected thread and frame. if (currentCmd->thread() == -1) currentCmd->setThread(frameStackModel()->currentThread()); if (currentCmd->frame() == -1) currentCmd->setFrame(frameStackModel()->currentFrame()); } QString commandText = currentCmd->cmdToSend(); bool bad_command = false; QString message; int length = commandText.length(); // No i18n for message since it's mainly for debugging. if (length == 0) { // The command might decide it's no longer necessary to send // it. if (SentinelCommand* sc = dynamic_cast(currentCmd)) { qCDebug(DEBUGGERCOMMON) << "SEND: sentinel command, not sending"; sc->invokeHandler(); } else { qCDebug(DEBUGGERCOMMON) << "SEND: command " << currentCmd->initialString() << "changed its mind, not sending"; } delete currentCmd; executeCmd(); return; } else { if (commandText[length-1] != '\n') { bad_command = true; message = QStringLiteral("Debugger command does not end with newline"); } } if (bad_command) { KMessageBox::information(qApp->activeWindow(), i18n("Invalid debugger command
%1", message), i18n("Invalid debugger command")); executeCmd(); return; } m_debugger->execute(currentCmd); } void MIDebugSession::ensureDebuggerListening() { Q_ASSERT(m_debugger); // Note: we don't use interruptDebugger() here since // we don't want to queue more commands before queuing a command m_debugger->interrupt(); setDebuggerStateOn(s_interruptSent); if (debuggerStateIsOn(s_appRunning)) setDebuggerStateOn(s_automaticContinue); setDebuggerStateOff(s_dbgNotListening); } void MIDebugSession::destroyCmds() { m_commandQueue->clear(); } // FIXME: I don't fully remember what is the business with // m_stateReloadInProgress and whether we can lift it to the // generic level. void MIDebugSession::raiseEvent(event_t e) { if (e == program_exited || e == debugger_exited) { m_stateReloadInProgress = false; } if (e == program_state_changed) { m_stateReloadInProgress = true; qCDebug(DEBUGGERCOMMON) << "State reload in progress\n"; } IDebugSession::raiseEvent(e); if (e == program_state_changed) { m_stateReloadInProgress = false; } } bool KDevMI::MIDebugSession::hasCrashed() const { return m_hasCrashed; } void MIDebugSession::slotDebuggerReady() { Q_ASSERT(m_debugger); m_stateReloadInProgress = false; executeCmd(); if (m_debugger->isReady()) { /* There is nothing in the command queue and no command is currently executing. */ if (debuggerStateIsOn(s_automaticContinue)) { if (!debuggerStateIsOn(s_appRunning)) { qCDebug(DEBUGGERCOMMON) << "Posting automatic continue"; addCommand(ExecContinue, QString(), CmdMaybeStartsRunning); } setDebuggerStateOff(s_automaticContinue); return; } if (m_stateReloadNeeded && !debuggerStateIsOn(s_appRunning)) { qCDebug(DEBUGGERCOMMON) << "Finishing program stop"; // Set to false right now, so that if 'actOnProgramPauseMI_part2' // sends some commands, we won't call it again when handling replies // from that commands. m_stateReloadNeeded = false; reloadProgramState(); } qCDebug(DEBUGGERCOMMON) << "No more commands"; setDebuggerStateOff(s_dbgBusy); raiseEvent(debugger_ready); } } void MIDebugSession::slotDebuggerExited(bool abnormal, const QString &msg) { /* Technically speaking, GDB is likely not to kill the application, and we should have some backup mechanism to make sure the application is killed by KDevelop. But even if application stays around, we no longer can control it in any way, so mark it as exited. */ setDebuggerStateOn(s_appNotStarted); setDebuggerStateOn(s_dbgNotStarted); setDebuggerStateOn(s_programExited); setDebuggerStateOff(s_shuttingDown); if (!msg.isEmpty()) emit showMessage(msg, 3000); if (abnormal) { /* The error is reported to user in MIDebugger now. KMessageBox::information( KDevelop::ICore::self()->uiController()->activeMainWindow(), i18n("Debugger exited abnormally" "

This is likely a bug in GDB. " "Examine the gdb output window and then stop the debugger"), i18n("Debugger exited abnormally")); */ // FIXME: not sure if the following still applies. // Note: we don't stop the debugger here, becuse that will hide gdb // window and prevent the user from finding the exact reason of the // problem. } /* FIXME: raiseEvent is handled across multiple places where we explicitly * stop/kill the debugger, a better way is to let the debugger itself report * its exited event. */ // raiseEvent(debugger_exited); } void MIDebugSession::slotInferiorStopped(const MI::AsyncRecord& r) { /* By default, reload all state on program stop. */ m_stateReloadNeeded = true; setDebuggerStateOff(s_appRunning); setDebuggerStateOff(s_dbgNotListening); QString reason; if (r.hasField(QStringLiteral("reason"))) reason = r[QStringLiteral("reason")].literal(); if (reason == QLatin1String("exited-normally") || reason == QLatin1String("exited")) { if (r.hasField(QStringLiteral("exit-code"))) { programNoApp(i18n("Exited with return code: %1", r["exit-code"].literal())); } else { programNoApp(i18n("Exited normally")); } m_stateReloadNeeded = false; return; } if (reason == QLatin1String("exited-signalled")) { programNoApp(i18n("Exited on signal %1", r["signal-name"].literal())); m_stateReloadNeeded = false; return; } if (reason == QLatin1String("watchpoint-scope")) { QString number = r[QStringLiteral("wpnum")].literal(); // FIXME: shuld remove this watchpoint // But first, we should consider if removing all // watchpoinst on program exit is the right thing to // do. addCommand(ExecContinue, QString(), CmdMaybeStartsRunning); m_stateReloadNeeded = false; return; } bool wasInterrupt = false; if (reason == QLatin1String("signal-received")) { QString name = r[QStringLiteral("signal-name")].literal(); QString user_name = r[QStringLiteral("signal-meaning")].literal(); // SIGINT is a "break into running program". // We do this when the user set/mod/clears a breakpoint but the // application is running. // And the user does this to stop the program also. if (name == QLatin1String("SIGINT") && debuggerStateIsOn(s_interruptSent)) { wasInterrupt = true; } else { // Whenever we have a signal raised then tell the user, but don't // end the program as we want to allow the user to look at why the // program has a signal that's caused the prog to stop. // Continuing from SIG FPE/SEGV will cause a "Cannot ..." and // that'll end the program. programFinished(i18n("Program received signal %1 (%2)", name, user_name)); m_hasCrashed = true; } } if (!reason.contains(QLatin1String("exited"))) { // FIXME: we should immediately update the current thread and // frame in the framestackmodel, so that any user actions // are in that thread. However, the way current framestack model // is implemented, we can't change thread id until we refresh // the entire list of threads -- otherwise we might set a thread // id that is not already in the list, and it will be upset. //Indicates if program state should be reloaded immediately. bool updateState = false; if (r.hasField(QStringLiteral("frame"))) { const MI::Value& frame = r[QStringLiteral("frame")]; QString file, line, addr; if (frame.hasField(QStringLiteral("fullname"))) file = frame[QStringLiteral("fullname")].literal(); if (frame.hasField(QStringLiteral("line"))) line = frame[QStringLiteral("line")].literal(); if (frame.hasField(QStringLiteral("addr"))) addr = frame[QStringLiteral("addr")].literal(); // gdb counts lines from 1 and we don't setCurrentPosition(QUrl::fromLocalFile(file), line.toInt() - 1, addr); updateState = true; } if (updateState) { reloadProgramState(); } } setDebuggerStateOff(s_interruptSent); if (!wasInterrupt) setDebuggerStateOff(s_automaticContinue); } void MIDebugSession::slotInferiorRunning() { setDebuggerStateOn(s_appRunning); raiseEvent(program_running); if (m_commandQueue->haveImmediateCommand() || (m_debugger->currentCommand() && (m_debugger->currentCommand()->flags() & (CmdImmediately | CmdInterrupt)))) { ensureDebuggerListening(); } else { setDebuggerStateOn(s_dbgNotListening); } } void MIDebugSession::processNotification(const MI::AsyncRecord & async) { if (async.reason == QLatin1String("thread-group-started")) { setDebuggerStateOff(s_appNotStarted | s_programExited); } else if (async.reason == QLatin1String("thread-group-exited")) { setDebuggerStateOn(s_programExited); } else if (async.reason == QLatin1String("library-loaded")) { // do nothing } else if (async.reason == QLatin1String("breakpoint-created")) { breakpointController()->notifyBreakpointCreated(async); } else if (async.reason == QLatin1String("breakpoint-modified")) { breakpointController()->notifyBreakpointModified(async); } else if (async.reason == QLatin1String("breakpoint-deleted")) { breakpointController()->notifyBreakpointDeleted(async); } else { qCDebug(DEBUGGERCOMMON) << "Unhandled notification: " << async.reason; } } void MIDebugSession::reloadProgramState() { raiseEvent(program_state_changed); m_stateReloadNeeded = false; } // There is no app anymore. This can be caused by program exiting // an invalid program specified or ... // gdb is still running though, but only the run command (may) make sense // all other commands are disabled. void MIDebugSession::programNoApp(const QString& msg) { qCDebug(DEBUGGERCOMMON) << msg; setDebuggerState(s_appNotStarted | s_programExited | (m_debuggerState & s_shuttingDown)); destroyCmds(); // The application has existed, but it's possible that // some of application output is still in the pipe. We use // different pipes to communicate with gdb and to get application // output, so "exited" message from gdb might have arrived before // last application output. Get this last bit. // Note: this method can be called when we open an invalid // core file. In that case, tty_ won't be set. if (m_tty){ m_tty->readRemaining(); // Tty is no longer usable, delete it. Without this, QSocketNotifier // will continuously bomd STTY with signals, so we need to either disable // QSocketNotifier, or delete STTY. The latter is simpler, since we can't // reuse it for future debug sessions anyway. m_tty.reset(nullptr); } stopDebugger(); raiseEvent(program_exited); raiseEvent(debugger_exited); emit showMessage(msg, 0); programFinished(msg); } void MIDebugSession::programFinished(const QString& msg) { QString m = QStringLiteral("*** %0 ***").arg(msg.trimmed()); emit inferiorStderrLines(QStringList(m)); /* Also show message in gdb window, so that users who prefer to look at gdb window know what's up. */ emit debuggerUserCommandOutput(m); } void MIDebugSession::explainDebuggerStatus() { MICommand* currentCmd_ = m_debugger->currentCommand(); QString information = i18np("1 command in queue\n", "%1 commands in queue\n", m_commandQueue->count()) + i18ncp("Only the 0 and 1 cases need to be translated", "1 command being processed by gdb\n", "%1 commands being processed by gdb\n", (currentCmd_ ? 1 : 0)) + i18n("Debugger state: %1\n", m_debuggerState); if (currentCmd_) { QString extra = i18n("Current command class: '%1'\n" "Current command text: '%2'\n" "Current command original text: '%3'\n", typeid(*currentCmd_).name(), currentCmd_->cmdToSend(), currentCmd_->initialString()); information += extra; } KMessageBox::information(qApp->activeWindow(), information, i18n("Debugger status")); } // There is no app anymore. This can be caused by program exiting // an invalid program specified or ... // gdb is still running though, but only the run command (may) make sense // all other commands are disabled. void MIDebugSession::handleNoInferior(const QString& msg) { qCDebug(DEBUGGERCOMMON) << msg; setDebuggerState(s_appNotStarted | s_programExited | (debuggerState() & s_shuttingDown)); destroyCmds(); // The application has existed, but it's possible that // some of application output is still in the pipe. We use // different pipes to communicate with gdb and to get application // output, so "exited" message from gdb might have arrived before // last application output. Get this last bit. // Note: this method can be called when we open an invalid // core file. In that case, tty_ won't be set. if (m_tty){ m_tty->readRemaining(); // Tty is no longer usable, delete it. Without this, QSocketNotifier // will continuously bomd STTY with signals, so we need to either disable // QSocketNotifier, or delete STTY. The latter is simpler, since we can't // reuse it for future debug sessions anyway. m_tty.reset(nullptr); } stopDebugger(); raiseEvent(program_exited); raiseEvent(debugger_exited); emit showMessage(msg, 0); handleInferiorFinished(msg); } void MIDebugSession::handleInferiorFinished(const QString& msg) { QString m = QStringLiteral("*** %0 ***").arg(msg.trimmed()); emit inferiorStderrLines(QStringList(m)); /* Also show message in gdb window, so that users who prefer to look at gdb window know what's up. */ emit debuggerUserCommandOutput(m); } // FIXME: connect to debugger's slot. void MIDebugSession::defaultErrorHandler(const MI::ResultRecord& result) { QString msg = result[QStringLiteral("msg")].literal(); if (msg.contains(QLatin1String("No such process"))) { setDebuggerState(s_appNotStarted|s_programExited); raiseEvent(program_exited); return; } KMessageBox::information( qApp->activeWindow(), i18n("Debugger error" "

Debugger reported the following error:" "

%1", result["msg"].literal()), i18n("Debugger error")); // Error most likely means that some change made in GUI // was not communicated to the gdb, so GUI is now not // in sync with gdb. Resync it. // // Another approach is to make each widget reload it content // on errors from commands that it sent, but that's too complex. // Errors are supposed to happen rarely, so full reload on error // is not a big deal. Well, maybe except for memory view, but // it's no auto-reloaded anyway. // // Also, don't reload state on errors appeared during state // reloading! if (!m_debugger->currentCommand()->stateReloading()) raiseEvent(program_state_changed); } void MIDebugSession::setSourceInitFile(bool enable) { m_sourceInitFile = enable; }