diff --git a/language/backgroundparser/backgroundparser.cpp b/language/backgroundparser/backgroundparser.cpp index dcadbaeec4..1bf9cd5216 100644 --- a/language/backgroundparser/backgroundparser.cpp +++ b/language/backgroundparser/backgroundparser.cpp @@ -1,852 +1,854 @@ /* * This file is part of KDevelop * * Copyright 2006 Adam Treat * Copyright 2007 Kris Wong * Copyright 2007-2008 David Nolden * * 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 "backgroundparser.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "util/debug.h" #include "parsejob.h" #include const bool separateThreadForHighPriority = true; /** * Elides string in @p path, e.g. "VEEERY/LONG/PATH" -> ".../LONG/PATH" * - probably much faster than QFontMetrics::elidedText() * - we dont need a widget context * - takes path separators into account * * @p width Maximum number of characters * * TODO: Move to kdevutil? */ static QString elidedPathLeft(const QString& path, int width) { static const QChar separator = QDir::separator(); static const QString placeholder = QStringLiteral("..."); if (path.size() <= width) { return path; } int start = (path.size() - width) + placeholder.size(); int pos = path.indexOf(separator, start); if (pos == -1) { pos = start; // no separator => just cut off the path at the beginning } Q_ASSERT(path.size() - pos >= 0 && path.size() - pos <= width); QStringRef elidedText = path.rightRef(path.size() - pos); QString result = placeholder; result.append(elidedText); return result; } namespace { /** * @return true if @p url is non-empty, valid and has a clean path, false otherwise. */ inline bool isValidURL(const KDevelop::IndexedString& url) { if (url.isEmpty()) { return false; } QUrl original = url.toUrl(); if (!original.isValid() || original.isRelative() || original.fileName().isEmpty()) { qCWarning(LANGUAGE) << "INVALID URL ENCOUNTERED:" << url << original; return false; } QUrl cleaned = original.adjusted(QUrl::NormalizePathSegments); return original == cleaned; } } namespace KDevelop { class BackgroundParserPrivate { public: BackgroundParserPrivate(BackgroundParser *parser, ILanguageController *languageController) :m_parser(parser), m_languageController(languageController), m_shuttingDown(false), m_mutex(QMutex::Recursive) { parser->d = this; //Set this so we can safely call back BackgroundParser from within loadSettings() m_timer.setSingleShot(true); m_delay = 500; m_threads = 1; m_doneParseJobs = 0; m_maxParseJobs = 0; m_neededPriority = BackgroundParser::WorstPriority; ThreadWeaver::setDebugLevel(true, 1); QObject::connect(&m_timer, &QTimer::timeout, m_parser, &BackgroundParser::parseDocuments); } void startTimerThreadSafe(int delay) { QMetaObject::invokeMethod(m_parser, "startTimer", Qt::QueuedConnection, Q_ARG(int, delay)); } ~BackgroundParserPrivate() { + m_weaver.resume(); + m_weaver.finish(); } // Non-mutex guarded functions, only call with m_mutex acquired. void parseDocumentsInternal() { if(m_shuttingDown) return; // Create delayed jobs, that is, jobs for documents which have been changed // by the user. QList jobs; // Before starting a new job, first wait for all higher-priority ones to finish. // That way, parse job priorities can be used for dependency handling. int bestRunningPriority = BackgroundParser::WorstPriority; foreach (const ThreadWeaver::QObjectDecorator* decorator, m_parseJobs) { const ParseJob* parseJob = dynamic_cast(decorator->job()); Q_ASSERT(parseJob); if (parseJob->respectsSequentialProcessing() && parseJob->parsePriority() < bestRunningPriority) { bestRunningPriority = parseJob->parsePriority(); } } bool done = false; for (QMap >::Iterator it1 = m_documentsForPriority.begin(); it1 != m_documentsForPriority.end(); ++it1 ) { if(it1.key() > m_neededPriority) break; //The priority is not good enough to be processed right now for(QSet::Iterator it = it1.value().begin(); it != it1.value().end();) { //Only create parse-jobs for up to thread-count * 2 documents, so we don't fill the memory unnecessarily if(m_parseJobs.count() >= m_threads+1 || (m_parseJobs.count() >= m_threads && !separateThreadForHighPriority) ) break; if(m_parseJobs.count() >= m_threads && it1.key() > BackgroundParser::NormalPriority && !specialParseJob) break; //The additional parsing thread is reserved for higher priority parsing // When a document is scheduled for parsing while it is being parsed, it will be parsed // again once the job finished, but not now. if (m_parseJobs.contains(*it) ) { ++it; continue; } Q_ASSERT(m_documents.contains(*it)); const DocumentParsePlan& parsePlan = m_documents[*it]; // If the current job requires sequential processing, but not all jobs with a better priority have been // completed yet, it will not be created now. if ( parsePlan.sequentialProcessingFlags() & ParseJob::RequiresSequentialProcessing && parsePlan.priority() > bestRunningPriority ) { ++it; continue; } qCDebug(LANGUAGE) << "creating parse-job" << *it << "new count of active parse-jobs:" << m_parseJobs.count() + 1; const QString elidedPathString = elidedPathLeft(it->str(), 70); emit m_parser->showMessage(m_parser, i18n("Parsing: %1", elidedPathString)); ThreadWeaver::QObjectDecorator* decorator = createParseJob(*it, parsePlan.features(), parsePlan.notifyWhenReady(), parsePlan.priority()); if(m_parseJobs.count() == m_threads+1 && !specialParseJob) specialParseJob = decorator; //This parse-job is allocated into the reserved thread if (decorator) { ParseJob* parseJob = dynamic_cast(decorator->job()); parseJob->setSequentialProcessingFlags(parsePlan.sequentialProcessingFlags()); jobs.append(ThreadWeaver::JobPointer(decorator)); // update the currently best processed priority, if the created job respects sequential processing if ( parsePlan.sequentialProcessingFlags() & ParseJob::RespectsSequentialProcessing && parsePlan.priority() < bestRunningPriority) { bestRunningPriority = parsePlan.priority(); } } // Remove all mentions of this document. foreach(const DocumentParseTarget& target, parsePlan.targets) { if (target.priority != it1.key()) { m_documentsForPriority[target.priority].remove(*it); } } m_documents.remove(*it); it = it1.value().erase(it); --m_maxParseJobs; //We have added one when putting the document into m_documents if(!m_documents.isEmpty()) { // Only try creating one parse-job at a time, else we might iterate through thousands of files // without finding a language-support, and block the UI for a long time. // If there are more documents to parse, instantly re-try. QMetaObject::invokeMethod(m_parser, "parseDocuments", Qt::QueuedConnection); done = true; break; } } if ( done ) break; } // Ok, enqueueing is fine because m_parseJobs contains all of the jobs now foreach (const ThreadWeaver::JobPointer& job, jobs) m_weaver.enqueue(job); m_parser->updateProgressBar(); //We don't hide the progress-bar in updateProgressBar, so it doesn't permanently flash when a document is reparsed again and again. if(m_doneParseJobs == m_maxParseJobs || (m_neededPriority == BackgroundParser::BestPriority && m_weaver.queueLength() == 0)) { emit m_parser->hideProgress(m_parser); } } ThreadWeaver::QObjectDecorator* createParseJob(const IndexedString& url, TopDUContext::Features features, const QList >& notifyWhenReady, int priority = 0) { ///FIXME: use IndexedString in the other APIs as well! Esp. for createParseJob! QUrl qUrl = url.toUrl(); auto languages = m_languageController->languagesForUrl(qUrl); foreach (const auto language, languages) { if(!language) { qCWarning(LANGUAGE) << "got zero language for" << qUrl; continue; } ParseJob* job = language->createParseJob(url); if (!job) { continue; // Language part did not produce a valid ParseJob. } job->setParsePriority(priority); job->setMinimumFeatures(features); job->setNotifyWhenReady(notifyWhenReady); ThreadWeaver::QObjectDecorator* decorator = new ThreadWeaver::QObjectDecorator(job); QObject::connect(decorator, &ThreadWeaver::QObjectDecorator::done, m_parser, &BackgroundParser::parseComplete); QObject::connect(decorator, &ThreadWeaver::QObjectDecorator::failed, m_parser, &BackgroundParser::parseComplete); QObject::connect(job, &ParseJob::progress, m_parser, &BackgroundParser::parseProgress, Qt::QueuedConnection); m_parseJobs.insert(url, decorator); ++m_maxParseJobs; // TODO more thinking required here to support multiple parse jobs per url (where multiple language plugins want to parse) return decorator; } if(languages.isEmpty()) qCDebug(LANGUAGE) << "found no languages for url" << qUrl; else qCDebug(LANGUAGE) << "could not create parse-job for url" << qUrl; //Notify that we failed typedef QPointer Notify; foreach(const Notify& n, notifyWhenReady) if(n) QMetaObject::invokeMethod(n.data(), "updateReady", Qt::QueuedConnection, Q_ARG(KDevelop::IndexedString, url), Q_ARG(KDevelop::ReferencedTopDUContext, ReferencedTopDUContext())); return nullptr; } void loadSettings() { ///@todo re-load settings when they have been changed! Q_ASSERT(ICore::self()->activeSession()); KConfigGroup config(ICore::self()->activeSession()->config(), "Background Parser"); // stay backwards compatible KConfigGroup oldConfig(KSharedConfig::openConfig(), "Background Parser"); #define BACKWARDS_COMPATIBLE_ENTRY(entry, default) \ config.readEntry(entry, oldConfig.readEntry(entry, default)) m_delay = BACKWARDS_COMPATIBLE_ENTRY("Delay", 500); m_timer.setInterval(m_delay); m_threads = 0; if (qEnvironmentVariableIsSet("KDEV_BACKGROUNDPARSER_MAXTHREADS")) { m_parser->setThreadCount(qgetenv("KDEV_BACKGROUNDPARSER_MAXTHREADS").toInt()); } else { m_parser->setThreadCount(BACKWARDS_COMPATIBLE_ENTRY("Number of Threads", QThread::idealThreadCount())); } resume(); if (BACKWARDS_COMPATIBLE_ENTRY("Enabled", true)) { m_parser->enableProcessing(); } else { m_parser->disableProcessing(); } } void suspend() { qCDebug(LANGUAGE) << "Suspending background parser"; bool s = m_weaver.state()->stateId() == ThreadWeaver::Suspended || m_weaver.state()->stateId() == ThreadWeaver::Suspending; if (s) { // Already suspending qCWarning(LANGUAGE) << "Already suspended or suspending"; return; } m_timer.stop(); m_weaver.suspend(); } void resume() { bool s = m_weaver.state()->stateId() == ThreadWeaver::Suspended || m_weaver.state()->stateId() == ThreadWeaver::Suspending; if (m_timer.isActive() && !s) { // Not suspending return; } m_timer.start(m_delay); m_weaver.resume(); } BackgroundParser *m_parser; ILanguageController* m_languageController; //Current parse-job that is executed in the additional thread QPointer specialParseJob; QTimer m_timer; int m_delay; int m_threads; bool m_shuttingDown; struct DocumentParseTarget { QPointer notifyWhenReady; int priority; TopDUContext::Features features; ParseJob::SequentialProcessingFlags sequentialProcessingFlags; bool operator==(const DocumentParseTarget& rhs) const { return notifyWhenReady == rhs.notifyWhenReady && priority == rhs.priority && features == rhs.features; } }; struct DocumentParsePlan { QSet targets; ParseJob::SequentialProcessingFlags sequentialProcessingFlags() const { //Pick the strictest possible flags ParseJob::SequentialProcessingFlags ret = ParseJob::IgnoresSequentialProcessing; foreach(const DocumentParseTarget &target, targets) { ret |= target.sequentialProcessingFlags; } return ret; } int priority() const { //Pick the best priority int ret = BackgroundParser::WorstPriority; foreach(const DocumentParseTarget &target, targets) { if(target.priority < ret) { ret = target.priority; } } return ret; } TopDUContext::Features features() const { //Pick the best features TopDUContext::Features ret = (TopDUContext::Features)0; foreach(const DocumentParseTarget &target, targets) { ret = (TopDUContext::Features) (ret | target.features); } return ret; } QList > notifyWhenReady() const { QList > ret; foreach(const DocumentParseTarget &target, targets) if(target.notifyWhenReady) ret << target.notifyWhenReady; return ret; } }; // A list of documents that are planned to be parsed, and their priority QHash m_documents; // The documents ordered by priority QMap > m_documentsForPriority; // Currently running parse jobs QHash m_parseJobs; // The url for each managed document. Those may temporarily differ from the real url. QHash m_managedTextDocumentUrls; // Projects currently in progress of loading QSet m_loadingProjects; ThreadWeaver::Queue m_weaver; // generic high-level mutex QMutex m_mutex; // local mutex only protecting m_managed QMutex m_managedMutex; // A change tracker for each managed document QHash m_managed; int m_maxParseJobs; int m_doneParseJobs; QHash m_jobProgress; int m_neededPriority; //The minimum priority needed for processed jobs }; inline uint qHash(const BackgroundParserPrivate::DocumentParseTarget& target) { return target.features * 7 + target.priority * 13 + target.sequentialProcessingFlags * 17 + reinterpret_cast(target.notifyWhenReady.data()); }; BackgroundParser::BackgroundParser(ILanguageController *languageController) : QObject(languageController), d(new BackgroundParserPrivate(this, languageController)) { Q_ASSERT(ICore::self()->documentController()); connect(ICore::self()->documentController(), &IDocumentController::documentLoaded, this, &BackgroundParser::documentLoaded); connect(ICore::self()->documentController(), &IDocumentController::documentUrlChanged, this, &BackgroundParser::documentUrlChanged); connect(ICore::self()->documentController(), &IDocumentController::documentClosed, this, &BackgroundParser::documentClosed); connect(ICore::self(), &ICore::aboutToShutdown, this, &BackgroundParser::aboutToQuit); bool connected = QObject::connect(ICore::self()->projectController(), &IProjectController::projectAboutToBeOpened, this, &BackgroundParser::projectAboutToBeOpened); Q_ASSERT(connected); connected = QObject::connect(ICore::self()->projectController(), &IProjectController::projectOpened, this, &BackgroundParser::projectOpened); Q_ASSERT(connected); connected = QObject::connect(ICore::self()->projectController(), &IProjectController::projectOpeningAborted, this, &BackgroundParser::projectOpeningAborted); Q_ASSERT(connected); Q_UNUSED(connected); } void BackgroundParser::aboutToQuit() { d->m_shuttingDown = true; } BackgroundParser::~BackgroundParser() { delete d; } QString BackgroundParser::statusName() const { return i18n("Background Parser"); } void BackgroundParser::loadSettings() { d->loadSettings(); } void BackgroundParser::parseProgress(KDevelop::ParseJob* job, float value, QString text) { Q_UNUSED(text) d->m_jobProgress[job] = value; updateProgressBar(); } void BackgroundParser::revertAllRequests(QObject* notifyWhenReady) { QMutexLocker lock(&d->m_mutex); for(QHash::iterator it = d->m_documents.begin(); it != d->m_documents.end(); ) { d->m_documentsForPriority[it.value().priority()].remove(it.key()); foreach ( const BackgroundParserPrivate::DocumentParseTarget& target, (*it).targets ) { if ( notifyWhenReady && target.notifyWhenReady.data() == notifyWhenReady ) { (*it).targets.remove(target); } } if((*it).targets.isEmpty()) { it = d->m_documents.erase(it); --d->m_maxParseJobs; continue; } d->m_documentsForPriority[it.value().priority()].insert(it.key()); ++it; } } void BackgroundParser::addDocument(const IndexedString& url, TopDUContext::Features features, int priority, QObject* notifyWhenReady, ParseJob::SequentialProcessingFlags flags, int delay) { // qCDebug(LANGUAGE) << "BackgroundParser::addDocument" << url.toUrl(); Q_ASSERT(isValidURL(url)); QMutexLocker lock(&d->m_mutex); { BackgroundParserPrivate::DocumentParseTarget target; target.priority = priority; target.features = features; target.sequentialProcessingFlags = flags; target.notifyWhenReady = QPointer(notifyWhenReady); QHash::iterator it = d->m_documents.find(url); if (it != d->m_documents.end()) { //Update the stored plan d->m_documentsForPriority[it.value().priority()].remove(url); it.value().targets << target; d->m_documentsForPriority[it.value().priority()].insert(url); }else{ // qCDebug(LANGUAGE) << "BackgroundParser::addDocument: queuing" << cleanedUrl; d->m_documents[url].targets << target; d->m_documentsForPriority[d->m_documents[url].priority()].insert(url); ++d->m_maxParseJobs; //So the progress-bar waits for this document } if ( delay == ILanguageSupport::DefaultDelay ) { delay = d->m_delay; } d->startTimerThreadSafe(delay); } } void BackgroundParser::removeDocument(const IndexedString& url, QObject* notifyWhenReady) { Q_ASSERT(isValidURL(url)); QMutexLocker lock(&d->m_mutex); if(d->m_documents.contains(url)) { d->m_documentsForPriority[d->m_documents[url].priority()].remove(url); foreach(const BackgroundParserPrivate::DocumentParseTarget& target, d->m_documents[url].targets) { if(target.notifyWhenReady.data() == notifyWhenReady) { d->m_documents[url].targets.remove(target); } } if(d->m_documents[url].targets.isEmpty()) { d->m_documents.remove(url); --d->m_maxParseJobs; }else{ //Insert with an eventually different priority d->m_documentsForPriority[d->m_documents[url].priority()].insert(url); } } } void BackgroundParser::parseDocuments() { if (!d->m_loadingProjects.empty()) { startTimer(d->m_delay); return; } QMutexLocker lock(&d->m_mutex); d->parseDocumentsInternal(); } void BackgroundParser::parseComplete(const ThreadWeaver::JobPointer& job) { auto decorator = dynamic_cast(job.data()); Q_ASSERT(decorator); ParseJob* parseJob = dynamic_cast(decorator->job()); Q_ASSERT(parseJob); emit parseJobFinished(parseJob); { QMutexLocker lock(&d->m_mutex); d->m_parseJobs.remove(parseJob->document()); d->m_jobProgress.remove(parseJob); ++d->m_doneParseJobs; updateProgressBar(); } //Continue creating more parse-jobs QMetaObject::invokeMethod(this, "parseDocuments", Qt::QueuedConnection); } void BackgroundParser::disableProcessing() { setNeededPriority(BestPriority); } void BackgroundParser::enableProcessing() { setNeededPriority(WorstPriority); } int BackgroundParser::priorityForDocument(const IndexedString& url) const { Q_ASSERT(isValidURL(url)); QMutexLocker lock(&d->m_mutex); return d->m_documents[url].priority(); } bool BackgroundParser::isQueued(const IndexedString& url) const { Q_ASSERT(isValidURL(url)); QMutexLocker lock(&d->m_mutex); return d->m_documents.contains(url); } int BackgroundParser::queuedCount() const { QMutexLocker lock(&d->m_mutex); return d->m_documents.count(); } bool BackgroundParser::isIdle() const { QMutexLocker lock(&d->m_mutex); return d->m_documents.isEmpty() && d->m_weaver.isIdle(); } void BackgroundParser::setNeededPriority(int priority) { QMutexLocker lock(&d->m_mutex); d->m_neededPriority = priority; d->startTimerThreadSafe(d->m_delay); } void BackgroundParser::abortAllJobs() { qCDebug(LANGUAGE) << "Aborting all parse jobs"; d->m_weaver.requestAbort(); } void BackgroundParser::suspend() { d->suspend(); emit hideProgress(this); } void BackgroundParser::resume() { d->resume(); updateProgressBar(); } void BackgroundParser::updateProgressBar() { if (d->m_doneParseJobs >= d->m_maxParseJobs) { if(d->m_doneParseJobs > d->m_maxParseJobs) { qCDebug(LANGUAGE) << "m_doneParseJobs larger than m_maxParseJobs:" << d->m_doneParseJobs << d->m_maxParseJobs; } d->m_doneParseJobs = 0; d->m_maxParseJobs = 0; } else { float additionalProgress = 0; for(QHash::const_iterator it = d->m_jobProgress.constBegin(); it != d->m_jobProgress.constEnd(); ++it) additionalProgress += *it; emit showProgress(this, 0, d->m_maxParseJobs*1000, (additionalProgress + d->m_doneParseJobs)*1000); } } ParseJob* BackgroundParser::parseJobForDocument(const IndexedString& document) const { Q_ASSERT(isValidURL(document)); QMutexLocker lock(&d->m_mutex); auto decorator = d->m_parseJobs.value(document); return decorator ? dynamic_cast(decorator->job()) : nullptr; } void BackgroundParser::setThreadCount(int threadCount) { if (d->m_threads != threadCount) { d->m_threads = threadCount; d->m_weaver.setMaximumNumberOfThreads(d->m_threads+1); //1 Additional thread for high-priority parsing } } int BackgroundParser::threadCount() const { return d->m_threads; } void BackgroundParser::setDelay(int miliseconds) { if (d->m_delay != miliseconds) { d->m_delay = miliseconds; d->m_timer.setInterval(d->m_delay); } } QList< IndexedString > BackgroundParser::managedDocuments() { QMutexLocker l(&d->m_managedMutex); return d->m_managed.keys(); } DocumentChangeTracker* BackgroundParser::trackerForUrl(const KDevelop::IndexedString& url) const { if (url.isEmpty()) { // this happens e.g. when setting the final location of a problem that is not // yet associated with a top ctx. return 0; } if ( !isValidURL(url) ) { qWarning() << "Tracker requested for invalild URL:" << url.toUrl(); } Q_ASSERT(isValidURL(url)); QMutexLocker l(&d->m_managedMutex); return d->m_managed.value(url, 0); } void BackgroundParser::documentClosed(IDocument* document) { QMutexLocker l(&d->m_mutex); if(document->textDocument()) { KTextEditor::Document* textDocument = document->textDocument(); if(!d->m_managedTextDocumentUrls.contains(textDocument)) return; // Probably the document had an invalid url, and thus it wasn't added to the background parser Q_ASSERT(d->m_managedTextDocumentUrls.contains(textDocument)); IndexedString url(d->m_managedTextDocumentUrls[textDocument]); QMutexLocker l2(&d->m_managedMutex); Q_ASSERT(d->m_managed.contains(url)); qCDebug(LANGUAGE) << "removing" << url.str() << "from background parser"; delete d->m_managed[url]; d->m_managedTextDocumentUrls.remove(textDocument); d->m_managed.remove(url); } } void BackgroundParser::documentLoaded( IDocument* document ) { QMutexLocker l(&d->m_mutex); if(document->textDocument() && document->textDocument()->url().isValid()) { KTextEditor::Document* textDocument = document->textDocument(); IndexedString url(document->url()); // Some debugging because we had issues with this QMutexLocker l2(&d->m_managedMutex); if(d->m_managed.contains(url) && d->m_managed[url]->document() == textDocument) { qCDebug(LANGUAGE) << "Got redundant documentLoaded from" << document->url() << textDocument; return; } qCDebug(LANGUAGE) << "Creating change tracker for " << document->url(); Q_ASSERT(!d->m_managed.contains(url)); Q_ASSERT(!d->m_managedTextDocumentUrls.contains(textDocument)); d->m_managedTextDocumentUrls[textDocument] = url; d->m_managed.insert(url, new DocumentChangeTracker(textDocument)); }else{ qCDebug(LANGUAGE) << "NOT creating change tracker for" << document->url(); } } void BackgroundParser::documentUrlChanged(IDocument* document) { documentClosed(document); // Only call documentLoaded if the file wasn't renamed to a filename that is already tracked. if(document->textDocument() && !trackerForUrl(IndexedString(document->textDocument()->url()))) documentLoaded(document); } void BackgroundParser::startTimer(int delay) { d->m_timer.start(delay); } void BackgroundParser::projectAboutToBeOpened(IProject* project) { d->m_loadingProjects.insert(project); } void BackgroundParser::projectOpened(IProject* project) { d->m_loadingProjects.remove(project); } void BackgroundParser::projectOpeningAborted(IProject* project) { d->m_loadingProjects.remove(project); } } Q_DECLARE_TYPEINFO(KDevelop::BackgroundParserPrivate::DocumentParseTarget, Q_MOVABLE_TYPE); Q_DECLARE_TYPEINFO(KDevelop::BackgroundParserPrivate::DocumentParsePlan, Q_MOVABLE_TYPE); diff --git a/serialization/indexedstring.cpp b/serialization/indexedstring.cpp index e4b4b652a9..4365433ad1 100644 --- a/serialization/indexedstring.cpp +++ b/serialization/indexedstring.cpp @@ -1,401 +1,401 @@ /* This file is part of KDevelop Copyright 2008 David Nolden Copyright 2016 Milian Wolff 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) 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, see . */ #include "indexedstring.h" #include "serialization/stringrepository.h" #include "referencecounting.h" using namespace KDevelop; namespace { struct IndexedStringData { unsigned short length; uint refCount; uint itemSize() const { return sizeof(IndexedStringData) + length; } uint hash() const { IndexedString::RunningHash running; const char* str = ((const char*)this) + sizeof(IndexedStringData); for (int a = length - 1; a >= 0; --a) { running.append(*str); ++str; } return running.hash; } }; inline void increase(uint& val) { ++val; } inline void decrease(uint& val) { --val; } struct IndexedStringRepositoryItemRequest { //The text is supposed to be utf8 encoded IndexedStringRepositoryItemRequest(const char* text, uint hash, unsigned short length) : m_hash(hash) , m_length(length) , m_text(text) { } enum { AverageSize = 10 //This should be the approximate average size of an Item }; typedef uint HashType; //Should return the hash-value associated with this request(For example the hash of a string) HashType hash() const { return m_hash; } //Should return the size of an item created with createItem uint itemSize() const { return sizeof(IndexedStringData) + m_length; } //Should create an item where the information of the requested item is permanently stored. The pointer //@param item equals an allocated range with the size of itemSize(). void createItem(IndexedStringData* item) const { item->length = m_length; item->refCount = 0; ++item; memcpy(item, m_text, m_length); } static void destroy(IndexedStringData* item, AbstractItemRepository&) { Q_UNUSED(item); //Nothing to do here (The object is not intelligent) } static bool persistent(const IndexedStringData* item) { return (bool)item->refCount; } //Should return whether the here requested item equals the given item bool equals(const IndexedStringData* item) const { return item->length == m_length && (memcmp(++item, m_text, m_length) == 0); } uint m_hash; unsigned short m_length; const char* m_text; }; inline const char* c_strFromItem(const IndexedStringData* item) { return reinterpret_cast(item + 1); } ///@param item must be valid(nonzero) inline QString stringFromItem(const IndexedStringData* item) { return QString::fromUtf8(c_strFromItem(item), item->length); } inline QByteArray arrayFromItem(const IndexedStringData* item) { return QByteArray(c_strFromItem(item), item->length); } inline bool isSingleCharIndex(uint index) { return (index & 0xffff0000) == 0xffff0000; } inline uint charToIndex(char c) { return 0xffff0000 | c; } inline char indexToChar(uint index) { Q_ASSERT(isSingleCharIndex(index)); return static_cast(index & 0xff); } using IndexedStringRepository = ItemRepository; using IndexedStringRepositoryManagerBase = RepositoryManager; class IndexedStringRepositoryManager : public IndexedStringRepositoryManagerBase { public: IndexedStringRepositoryManager() : IndexedStringRepositoryManagerBase(QStringLiteral("String Index")) { repository()->setMutex(&m_mutex); } private: // non-recursive mutex to increase speed QMutex m_mutex; }; IndexedStringRepository* globalIndexedStringRepository() { static IndexedStringRepositoryManager manager; return manager.repository(); } template auto readRepo(ReadAction action) -> decltype(action(globalIndexedStringRepository())) { const auto* repo = globalIndexedStringRepository(); QMutexLocker lock(repo->mutex()); return action(repo); } template auto editRepo(EditAction action) -> decltype(action(globalIndexedStringRepository())) { auto* repo = globalIndexedStringRepository(); QMutexLocker lock(repo->mutex()); return action(repo); } inline void ref(IndexedString* string) { const uint index = string->index(); if (index && !isSingleCharIndex(index)) { if (shouldDoDUChainReferenceCounting(string)) { editRepo([index] (IndexedStringRepository* repo) { increase(repo->dynamicItemFromIndexSimple(index)->refCount); }); } } } inline void deref(IndexedString* string) { const uint index = string->index(); if (index && !isSingleCharIndex(index)) { if (shouldDoDUChainReferenceCounting(string)) { editRepo([index] (IndexedStringRepository* repo) { decrease(repo->dynamicItemFromIndexSimple(index)->refCount); }); } } } } IndexedString::IndexedString() : m_index(0) { } ///@param str must be a utf8 encoded string, does not need to be 0-terminated. ///@param length must be its length in bytes. IndexedString::IndexedString(const char* str, unsigned short length, uint hash) { if (!length) { m_index = 0; } else if (length == 1) { m_index = charToIndex(str[0]); } else { const auto request = IndexedStringRepositoryItemRequest(str, hash ? hash : hashString(str, length), length); bool refcount = shouldDoDUChainReferenceCounting(this); m_index = editRepo([request, refcount] (IndexedStringRepository* repo) { auto index = repo->index(request); if (refcount) { increase(repo->dynamicItemFromIndexSimple(index)->refCount); } return index; }); } } IndexedString::IndexedString(char c) : m_index(charToIndex(c)) {} IndexedString::IndexedString(const QUrl& url) : IndexedString(url.isLocalFile() ? url.toLocalFile() : url.toString()) { Q_ASSERT(url.isEmpty() || !url.isRelative()); } IndexedString::IndexedString(const QString& string) : IndexedString(string.toUtf8()) {} IndexedString::IndexedString(const char* str) : IndexedString(str, str ? strlen(str) : 0) {} IndexedString::IndexedString(const QByteArray& str) : IndexedString(str.constData(), str.length()) {} IndexedString::~IndexedString() { deref(this); } IndexedString::IndexedString(const IndexedString& rhs) : m_index(rhs.m_index) { ref(this); } IndexedString& IndexedString::operator=(const IndexedString& rhs) { if (m_index == rhs.m_index) { return *this; } deref(this); m_index = rhs.m_index; ref(this); return *this; } QUrl IndexedString::toUrl() const { if (isEmpty()) { return {}; } QUrl ret = QUrl::fromUserInput(str()); Q_ASSERT(!ret.isRelative()); return ret; } QString IndexedString::str() const { if (!m_index) { return QString(); } else if (isSingleCharIndex(m_index)) { return QString(QLatin1Char(indexToChar(m_index))); } else { const uint index = m_index; return readRepo([index] (const IndexedStringRepository* repo) { return stringFromItem(repo->itemFromIndex(index)); }); } } int IndexedString::length() const { return lengthFromIndex(m_index); } int IndexedString::lengthFromIndex(uint index) { if (!index) { return 0; } else if (isSingleCharIndex(index)) { return 1; } else { return readRepo([index] (const IndexedStringRepository* repo) { return repo->itemFromIndex(index)->length; }); } } const char* IndexedString::c_str() const { if (!m_index) { return 0; } else if (isSingleCharIndex(m_index)) { -#if __BYTE_ORDER == __LITTLE_ENDIAN +#if Q_BYTE_ORDER == Q_LITTLE_ENDIAN const uint offset = 0; #else const uint offset = 3; #endif return reinterpret_cast(&m_index) + offset; } else { const uint index = m_index; return readRepo([index] (const IndexedStringRepository* repo) { return c_strFromItem(repo->itemFromIndex(index)); }); } } QByteArray IndexedString::byteArray() const { if (!m_index) { return QByteArray(); } else if (isSingleCharIndex(m_index)) { return QByteArray(1, indexToChar(m_index)); } else { const uint index = m_index; return readRepo([index] (const IndexedStringRepository* repo) { return arrayFromItem(repo->itemFromIndex(index)); }); } } uint IndexedString::hashString(const char* str, unsigned short length) { RunningHash running; for (int a = length - 1; a >= 0; --a) { running.append(*str); ++str; } return running.hash; } uint IndexedString::indexForString(const char* str, short unsigned length, uint hash) { if (!length) { return 0; } else if (length == 1) { return charToIndex(str[0]); } else { const auto request = IndexedStringRepositoryItemRequest(str, hash ? hash : hashString(str, length), length); return editRepo([request] (IndexedStringRepository* repo) { return repo->index(request); }); } } uint IndexedString::indexForString(const QString& str, uint hash) { const QByteArray array(str.toUtf8()); return indexForString(array.constBegin(), array.size(), hash); } QDebug operator<<(QDebug s, const IndexedString& string) { s.nospace() << string.str(); return s.space(); } diff --git a/shell/plugincontroller.cpp b/shell/plugincontroller.cpp index 651a93893c..d1819b6e32 100644 --- a/shell/plugincontroller.cpp +++ b/shell/plugincontroller.cpp @@ -1,762 +1,770 @@ /* This file is part of the KDE project Copyright 2004, 2007 Alexander Dymo Copyright 2006 Matt Rogers Based on code from Kopete Copyright (c) 2002-2003 Martijn Klingens 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 "plugincontroller.h" #include #include #include #include #include #include // TODO: remove once we no longer support old style plugins #include #include #include #include #include #include #include #include "core.h" #include "shellextension.h" #include "runcontroller.h" #include "debugcontroller.h" #include "documentationcontroller.h" #include "sourceformattercontroller.h" #include "projectcontroller.h" #include "ktexteditorpluginintegration.h" #include "debug.h" namespace { inline QString KEY_Plugins() { return QStringLiteral("Plugins"); } inline QString KEY_Suffix_Enabled() { return QStringLiteral("Enabled"); } inline QString KEY_LoadMode() { return QStringLiteral("X-KDevelop-LoadMode"); } inline QString KEY_Category() { return QStringLiteral("X-KDevelop-Category"); } inline QString KEY_Mode() { return QStringLiteral("X-KDevelop-Mode"); } inline QString KEY_Version() { return QStringLiteral("X-KDevelop-Version"); } inline QString KEY_Interfaces() { return QStringLiteral("X-KDevelop-Interfaces"); } inline QString KEY_Required() { return QStringLiteral("X-KDevelop-IRequired"); } inline QString KEY_Optional() { return QStringLiteral("X-KDevelop-IOptional"); } inline QString KEY_Global() { return QStringLiteral("Global"); } inline QString KEY_Project() { return QStringLiteral("Project"); } inline QString KEY_Gui() { return QStringLiteral("GUI"); } inline QString KEY_AlwaysOn() { return QStringLiteral("AlwaysOn"); } inline QString KEY_UserSelectable() { return QStringLiteral("UserSelectable"); } bool isUserSelectable( const KPluginMetaData& info ) { QString loadMode = info.value(KEY_LoadMode()); return loadMode.isEmpty() || loadMode == KEY_UserSelectable(); } bool isGlobalPlugin( const KPluginMetaData& info ) { return info.value(KEY_Category()) == KEY_Global(); } bool hasMandatoryProperties( const KPluginMetaData& info ) { QString mode = info.value(KEY_Mode()); if (mode.isEmpty()) { return false; } // when the plugin is installed into the versioned plugin path, it's good to go if (info.fileName().contains(QLatin1String("/kdevplatform/" QT_STRINGIFY(KDEVELOP_PLUGIN_VERSION) "/"))) { return true; } // the version property is only required when the plugin is not installed into the right directory QVariant version = info.rawData().value(KEY_Version()).toVariant(); if (version.isValid() && version.value() == KDEVELOP_PLUGIN_VERSION) { return true; } return false; } bool constraintsMatch( const KPluginMetaData& info, const QVariantMap& constraints) { for (auto it = constraints.begin(); it != constraints.end(); ++it) { const auto property = info.rawData().value(it.key()).toVariant(); if (!property.isValid()) { return false; } else if (property.canConvert()) { QSet values = property.toStringList().toSet(); QSet expected = it.value().toStringList().toSet(); if (!values.contains(expected)) { return false; } } else if (it.value() != property) { return false; } } return true; } struct Dependency { Dependency(const QString &dependency) : interface(dependency) { if (dependency.contains('@')) { const auto list = dependency.split('@', QString::SkipEmptyParts); if (list.size() == 2) { interface = list.at(0); pluginName = list.at(1); } } } QString interface; QString pluginName; }; } namespace KDevelop { class PluginControllerPrivate { public: QVector plugins; //map plugin infos to currently loaded plugins typedef QHash InfoToPluginMap; InfoToPluginMap loadedPlugins; // The plugin manager's mode. The mode is StartingUp until loadAllPlugins() // has finished loading the plugins, after which it is set to Running. // ShuttingDown and DoneShutdown are used during shutdown by the // async unloading of plugins. enum CleanupMode { Running /**< the plugin manager is running */, CleaningUp /**< the plugin manager is cleaning up for shutdown */, CleanupDone /**< the plugin manager has finished cleaning up */ }; CleanupMode cleanupMode; bool canUnload(const KPluginMetaData& plugin) { qCDebug(SHELL) << "checking can unload for:" << plugin.name() << plugin.value(KEY_LoadMode()); if (plugin.value(KEY_LoadMode()) == KEY_AlwaysOn()) { return false; } const QStringList interfaces = KPluginMetaData::readStringList(plugin.rawData(), KEY_Interfaces()); qCDebug(SHELL) << "checking dependencies:" << interfaces; foreach (const KPluginMetaData& info, loadedPlugins.keys()) { if (info.pluginId() != plugin.pluginId()) { QStringList dependencies = KPluginMetaData::readStringList(plugin.rawData(), KEY_Required()); dependencies += KPluginMetaData::readStringList(plugin.rawData(), KEY_Optional()); foreach (const QString& dep, dependencies) { Dependency dependency(dep); if (!dependency.pluginName.isEmpty() && dependency.pluginName != plugin.pluginId()) { continue; } if (interfaces.contains(dependency.interface) && !canUnload(info)) { return false; } } } } return true; } KPluginMetaData infoForId( const QString& id ) const { foreach (const KPluginMetaData& info, plugins) { if (info.pluginId() == id) { return info; } } return KPluginMetaData(); } /** * Iterate over all cached plugin infos, and call the functor for every enabled plugin. * * If an extension and/or pluginName is given, the functor will only be called for * those plugins matching this information. * * The functor should return false when the iteration can be stopped, and true if it * should be continued. */ template void foreachEnabledPlugin(F func, const QString &extension = {}, const QVariantMap& constraints = QVariantMap(), const QString &pluginName = {}) { foreach (const auto& info, plugins) { if ((pluginName.isEmpty() || info.pluginId() == pluginName) && (extension.isEmpty() || KPluginMetaData::readStringList(info.rawData(), KEY_Interfaces()).contains(extension)) && constraintsMatch(info, constraints) && isEnabled(info)) { if (!func(info)) { break; } } } } bool isEnabled(const KPluginMetaData& info) const { static const QStringList disabledPlugins = QString::fromLatin1(qgetenv("KDEV_DISABLE_PLUGINS")).split(';'); if (disabledPlugins.contains(info.pluginId())) { return false; } if (!isUserSelectable( info )) return true; if (!isGlobalPlugin( info )) { QJsonValue enabledByDefault = info.rawData()[QStringLiteral("KPlugin")].toObject()[QStringLiteral("EnabledByDefault")]; return enabledByDefault.isNull() || enabledByDefault.toBool(); //We consider plugins enabled until specified otherwise } KConfigGroup grp = Core::self()->activeSession()->config()->group( KEY_Plugins() ); const bool isDefaultPlugin = ShellExtension::getInstance()->defaultPlugins().isEmpty() || ShellExtension::getInstance()->defaultPlugins().contains(info.pluginId()); bool isEnabled = grp.readEntry(info.pluginId() + KEY_Suffix_Enabled(), isDefaultPlugin); // qDebug() << "read config:" << info.pluginId() << isEnabled << "is global plugin:" << isGlobalPlugin( info ) << "default:" << ShellExtension::getInstance()->defaultPlugins().isEmpty() << ShellExtension::getInstance()->defaultPlugins().contains( info.pluginId() ); return isEnabled; } Core *core; }; PluginController::PluginController(Core *core) : IPluginController(), d(new PluginControllerPrivate) { setObjectName(QStringLiteral("PluginController")); d->core = core; QSet foundPlugins; auto newPlugins = KPluginLoader::findPlugins(QStringLiteral("kdevplatform/" QT_STRINGIFY(KDEVELOP_PLUGIN_VERSION)), [&](const KPluginMetaData& meta) { if (meta.serviceTypes().contains(QStringLiteral("KDevelop/Plugin"))) { foundPlugins.insert(meta.pluginId()); return true; } else { qWarning() << "Plugin" << meta.fileName() << "is installed into the kdevplatform plugin directory, but does not have" " \"KDevelop/Plugin\" set as the service type. This plugin will not be loaded."; return false; } }); qCDebug(SHELL) << "Found" << newPlugins.size() << " plugins:" << foundPlugins; d->plugins = newPlugins; KTextEditorIntegration::initialize(); - const QVector katePlugins = KPluginLoader::findPlugins(QStringLiteral("ktexteditor"), [](const KPluginMetaData & md) { + const QVector ktePlugins = KPluginLoader::findPlugins(QStringLiteral("ktexteditor"), [](const KPluginMetaData & md) { return md.serviceTypes().contains(QStringLiteral("KTextEditor/Plugin")) && md.serviceTypes().contains(QStringLiteral("KDevelop/Plugin")); }); - foreach (const auto& info, katePlugins) { + + foundPlugins.clear(); + std::for_each(ktePlugins.cbegin(), ktePlugins.cend(), [&foundPlugins](const KPluginMetaData& data) { + foundPlugins << data.pluginId(); + }); + qCDebug(SHELL) << "Found" << ktePlugins.size() << " KTextEditor plugins:" << foundPlugins; + + foreach (const auto& info, ktePlugins) { auto data = info.rawData(); // add some KDevelop specific JSON data data[KEY_Category()] = KEY_Global(); data[KEY_Mode()] = KEY_Gui(); data[KEY_Version()] = KDEVELOP_PLUGIN_VERSION; d->plugins.append({data, info.fileName(), info.metaDataFileName()}); } d->cleanupMode = PluginControllerPrivate::Running; // Register the KDevelop::IPlugin* metatype so we can properly unload it qRegisterMetaType( "KDevelop::IPlugin*" ); } PluginController::~PluginController() { if ( d->cleanupMode != PluginControllerPrivate::CleanupDone ) { qCWarning(SHELL) << "Destructing plugin controller without going through the shutdown process!"; } delete d; } KPluginMetaData PluginController::pluginInfo( const IPlugin* plugin ) const { return d->loadedPlugins.key(const_cast(plugin)); } void PluginController::cleanup() { if(d->cleanupMode != PluginControllerPrivate::Running) { //qCDebug(SHELL) << "called when not running. state =" << d->cleanupMode; return; } d->cleanupMode = PluginControllerPrivate::CleaningUp; // Ask all plugins to unload while ( !d->loadedPlugins.isEmpty() ) { //Let the plugin do some stuff before unloading unloadPlugin(d->loadedPlugins.begin().value(), Now); } d->cleanupMode = PluginControllerPrivate::CleanupDone; } IPlugin* PluginController::loadPlugin( const QString& pluginName ) { return loadPluginInternal( pluginName ); } bool PluginController::isEnabled( const KPluginMetaData& info ) const { return d->isEnabled(info); } void PluginController::initialize() { QElapsedTimer timer; timer.start(); QMap pluginMap; if( ShellExtension::getInstance()->defaultPlugins().isEmpty() ) { foreach( const KPluginMetaData& pi, d->plugins ) { pluginMap.insert( pi.pluginId(), true ); } } else { // Get the default from the ShellExtension foreach( const QString& s, ShellExtension::getInstance()->defaultPlugins() ) { pluginMap.insert( s, true ); } } KConfigGroup grp = Core::self()->activeSession()->config()->group( KEY_Plugins() ); QMap entries = grp.entryMap(); QMap::Iterator it; for ( it = entries.begin(); it != entries.end(); ++it ) { const QString key = it.key(); if (key.endsWith(KEY_Suffix_Enabled())) { bool enabled = false; const QString pluginid = key.left(key.length() - 7); const bool defValue = pluginMap.value( pluginid, false ); enabled = grp.readEntry(key, defValue); pluginMap.insert( pluginid, enabled ); } } foreach( const KPluginMetaData& pi, d->plugins ) { if( isGlobalPlugin( pi ) ) { QMap::const_iterator it = pluginMap.constFind( pi.pluginId() ); if( it != pluginMap.constEnd() && ( it.value() || !isUserSelectable( pi ) ) ) { // Plugin is mentioned in pluginmap and the value is true, so try to load it loadPluginInternal( pi.pluginId() ); if(!grp.hasKey(pi.pluginId() + KEY_Suffix_Enabled())) { if( isUserSelectable( pi ) ) { // If plugin isn't listed yet, add it with true now grp.writeEntry(pi.pluginId() + KEY_Suffix_Enabled(), true); } } else if( grp.hasKey( pi.pluginId() + "Disabled" ) && !isUserSelectable( pi ) ) { // Remove now-obsolete entries grp.deleteEntry( pi.pluginId() + "Disabled" ); } } } } // Synchronize so we're writing out to the file. grp.sync(); qCDebug(SHELL) << "Done loading plugins - took:" << timer.elapsed() << "ms"; } QList PluginController::loadedPlugins() const { return d->loadedPlugins.values(); } bool PluginController::unloadPlugin( const QString & pluginId ) { IPlugin *thePlugin = plugin( pluginId ); bool canUnload = d->canUnload( d->infoForId( pluginId ) ); qCDebug(SHELL) << "Unloading plugin:" << pluginId << "?" << thePlugin << canUnload; if( thePlugin && canUnload ) { return unloadPlugin(thePlugin, Later); } return (canUnload && thePlugin); } bool PluginController::unloadPlugin(IPlugin* plugin, PluginDeletion deletion) { qCDebug(SHELL) << "unloading plugin:" << plugin << pluginInfo( plugin ).name(); emit unloadingPlugin(plugin); plugin->unload(); emit pluginUnloaded(plugin); //Remove the plugin from our list of plugins so we create a new //instance when we're asked for it again. //This is important to do right here, not later when the plugin really //vanishes. For example project re-opening might try to reload the plugin //and then would get the "old" pointer which will be deleted in the next //event loop run and thus causing crashes. for ( PluginControllerPrivate::InfoToPluginMap::Iterator it = d->loadedPlugins.begin(); it != d->loadedPlugins.end(); ++it ) { if ( it.value() == plugin ) { d->loadedPlugins.erase( it ); break; } } if (deletion == Later) plugin->deleteLater(); else delete plugin; return true; } KPluginMetaData PluginController::infoForPluginId( const QString &pluginId ) const { foreach (const KPluginMetaData& info, d->plugins) { if (info.pluginId() == pluginId) { return info; } } return KPluginMetaData(); } IPlugin *PluginController::loadPluginInternal( const QString &pluginId ) { QElapsedTimer timer; timer.start(); KPluginMetaData info = infoForPluginId( pluginId ); if ( !info.isValid() ) { qCWarning(SHELL) << "Unable to find a plugin named '" << pluginId << "'!" ; return nullptr; } if ( IPlugin* plugin = d->loadedPlugins.value( info ) ) { return plugin; } if ( !isEnabled( info ) ) { // Do not load disabled plugins qWarning() << "Not loading plugin named" << pluginId << "because it has been disabled!"; return nullptr; } if ( !hasMandatoryProperties( info ) ) { qWarning() << "Unable to load plugin named" << pluginId << "because not all mandatory properties are set."; return nullptr; } if ( info.value(KEY_Mode()) == KEY_Gui() && Core::self()->setupFlags() == Core::NoUi ) { qCDebug(SHELL) << "Not loading plugin named" << pluginId << "- Running in No-Ui mode, but the plugin says it needs a GUI"; return nullptr; } qCDebug(SHELL) << "Attempting to load" << pluginId << "- name:" << info.name(); emit loadingPlugin( info.pluginId() ); // first, ensure all dependencies are available and not disabled // this is unrelated to whether they are loaded already or not. // when we depend on e.g. A and B, but B cannot be found, then we // do not want to load A first and then fail on B and leave A loaded. // this would happen if we'd skip this step here and directly loadDependencies. QStringList missingInterfaces; if ( !hasUnresolvedDependencies( info, missingInterfaces ) ) { qWarning() << "Can't load plugin" << pluginId << "some of its required dependencies could not be fulfilled:" << missingInterfaces.join(QStringLiteral(",")); return nullptr; } // now ensure all dependencies are loaded QString failedDependency; if( !loadDependencies( info, failedDependency ) ) { qWarning() << "Can't load plugin" << pluginId << "because a required dependency could not be loaded:" << failedDependency; return nullptr; } // same for optional dependencies, but don't error out if anything fails loadOptionalDependencies( info ); // now we can finally load the plugin itself KPluginLoader loader(info.fileName()); auto factory = loader.factory(); if (!factory) { qWarning() << "Can't load plugin" << pluginId << "because a factory to load the plugin could not be obtained:" << loader.errorString(); return nullptr; } // now create it auto plugin = factory->create(d->core); if (!plugin) { if (auto katePlugin = factory->create(d->core, QVariantList() << info.pluginId())) { plugin = new KTextEditorIntegration::Plugin(katePlugin, d->core); } else { qWarning() << "Creating plugin" << pluginId << "failed."; return nullptr; } } KConfigGroup group = Core::self()->activeSession()->config()->group(KEY_Plugins()); // runtime errors such as missing executables on the system or such get checked now if (plugin->hasError()) { qWarning() << "Could not load plugin" << pluginId << ", it reported the error:" << plugin->errorDescription() << "Disabling the plugin now."; group.writeEntry(info.pluginId() + KEY_Suffix_Enabled(), false); // do the same as KPluginInfo did group.sync(); unloadPlugin(pluginId); return nullptr; } // yay, it all worked - the plugin is loaded d->loadedPlugins.insert(info, plugin); group.writeEntry(info.pluginId() + KEY_Suffix_Enabled(), true); // do the same as KPluginInfo did group.sync(); qCDebug(SHELL) << "Successfully loaded plugin" << pluginId << "from" << loader.fileName() << "- took:" << timer.elapsed() << "ms"; emit pluginLoaded( plugin ); return plugin; } IPlugin* PluginController::plugin( const QString& pluginId ) { KPluginMetaData info = infoForPluginId( pluginId ); if ( !info.isValid() ) return 0L; return d->loadedPlugins.value( info ); } bool PluginController::hasUnresolvedDependencies( const KPluginMetaData& info, QStringList& missing ) const { QSet required = KPluginMetaData::readStringList(info.rawData(), KEY_Required()).toSet(); if (!required.isEmpty()) { d->foreachEnabledPlugin([&required] (const KPluginMetaData& plugin) -> bool { foreach (const QString& iface, KPluginMetaData::readStringList(plugin.rawData(), KEY_Interfaces())) { required.remove(iface); required.remove(iface + '@' + plugin.pluginId()); } return !required.isEmpty(); }); } // if we found all dependencies required should be empty now if (!required.isEmpty()) { missing = required.toList(); return false; } return true; } void PluginController::loadOptionalDependencies( const KPluginMetaData& info ) { const QStringList dependencies = KPluginMetaData::readStringList(info.rawData(), KEY_Optional()); foreach (const QString& dep, dependencies) { Dependency dependency(dep); if (!pluginForExtension(dependency.interface, dependency.pluginName)) { qCDebug(SHELL) << "Couldn't load optional dependency:" << dep << info.pluginId(); } } } bool PluginController::loadDependencies( const KPluginMetaData& info, QString& failedDependency ) { const QStringList dependencies = KPluginMetaData::readStringList(info.rawData(), KEY_Required()); foreach (const QString& value, dependencies) { Dependency dependency(value); if (!pluginForExtension(dependency.interface, dependency.pluginName)) { failedDependency = value; return false; } } return true; } IPlugin *PluginController::pluginForExtension(const QString &extension, const QString &pluginName, const QVariantMap& constraints) { IPlugin* plugin = nullptr; d->foreachEnabledPlugin([this, &plugin] (const KPluginMetaData& info) -> bool { plugin = d->loadedPlugins.value( info ); if( !plugin ) { plugin = loadPluginInternal( info.pluginId() ); } return !plugin; }, extension, constraints, pluginName); return plugin; } QList PluginController::allPluginsForExtension(const QString &extension, const QVariantMap& constraints) { //qCDebug(SHELL) << "Finding all Plugins for Extension:" << extension << "|" << constraints; QList plugins; d->foreachEnabledPlugin([this, &plugins] (const KPluginMetaData& info) -> bool { IPlugin* plugin = d->loadedPlugins.value( info ); if( !plugin) { plugin = loadPluginInternal( info.pluginId() ); } - if (plugin) + if (plugin && !plugins.contains(plugin)) { plugins << plugin; + } return true; }, extension, constraints); return plugins; } QVector PluginController::queryExtensionPlugins(const QString& extension, const QVariantMap& constraints) const { QVector plugins; d->foreachEnabledPlugin([&plugins] (const KPluginMetaData& info) -> bool { plugins << info; return true; }, extension, constraints); return plugins; } QStringList PluginController::allPluginNames() { QStringList names; Q_FOREACH( const KPluginMetaData& info , d->plugins ) { names << info.pluginId(); } return names; } QList PluginController::queryPluginsForContextMenuExtensions( KDevelop::Context* context ) const { QList exts; for( auto it=d->loadedPlugins.constBegin(), itEnd = d->loadedPlugins.constEnd(); it!=itEnd; ++it ) { IPlugin* plug = it.value(); exts << plug->contextMenuExtension( context ); } exts << Core::self()->debugControllerInternal()->contextMenuExtension( context ); exts << Core::self()->documentationControllerInternal()->contextMenuExtension( context ); exts << Core::self()->sourceFormatterControllerInternal()->contextMenuExtension( context ); exts << Core::self()->runControllerInternal()->contextMenuExtension( context ); exts << Core::self()->projectControllerInternal()->contextMenuExtension( context ); return exts; } QStringList PluginController::projectPlugins() { QStringList names; foreach (const KPluginMetaData& info, d->plugins) { if (info.value(KEY_Category()) == KEY_Project()) { names << info.pluginId(); } } return names; } void PluginController::loadProjectPlugins() { Q_FOREACH( const QString& name, projectPlugins() ) { loadPluginInternal( name ); } } void PluginController::unloadProjectPlugins() { Q_FOREACH( const QString& name, projectPlugins() ) { unloadPlugin( name ); } } QVector PluginController::allPluginInfos() const { return d->plugins; } void PluginController::updateLoadedPlugins() { QStringList defaultPlugins = ShellExtension::getInstance()->defaultPlugins(); KConfigGroup grp = Core::self()->activeSession()->config()->group( KEY_Plugins() ); foreach( const KPluginMetaData& info, d->plugins ) { if( isGlobalPlugin( info ) ) { bool enabled = grp.readEntry(info.pluginId() + KEY_Suffix_Enabled(), ( defaultPlugins.isEmpty() || defaultPlugins.contains( info.pluginId() ) ) ) || !isUserSelectable( info ); bool loaded = d->loadedPlugins.contains( info ); if( loaded && !enabled ) { qCDebug(SHELL) << "unloading" << info.pluginId(); if( !unloadPlugin( info.pluginId() ) ) { grp.writeEntry( info.pluginId() + KEY_Suffix_Enabled(), false ); } } else if( !loaded && enabled ) { loadPluginInternal( info.pluginId() ); } } } } void PluginController::resetToDefaults() { KSharedConfigPtr cfg = Core::self()->activeSession()->config(); cfg->deleteGroup( KEY_Plugins() ); cfg->sync(); KConfigGroup grp = cfg->group( KEY_Plugins() ); QStringList plugins = ShellExtension::getInstance()->defaultPlugins(); if( plugins.isEmpty() ) { foreach( const KPluginMetaData& info, d->plugins ) { plugins << info.pluginId(); } } foreach( const QString& s, plugins ) { grp.writeEntry(s + KEY_Suffix_Enabled(), true); } grp.sync(); } } diff --git a/util/kdevplatform_shell_environment.sh b/util/kdevplatform_shell_environment.sh index e4e0c396e8..bb8d707c61 100755 --- a/util/kdevplatform_shell_environment.sh +++ b/util/kdevplatform_shell_environment.sh @@ -1,808 +1,808 @@ #!/bin/bash # This file is part of KDevelop # Copyright 2011 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 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. if [ -e ~/.bashrc ]; then # Since this runs as a replacement for the init-file, we need to chain in the 'real' bash-rc source ~/.bashrc fi if ! [ "$APPLICATION_HOST" ]; then export APPLICATION_HOST=$(hostname) fi if ! [ "$KDEV_SHELL_ENVIRONMENT_ID" ]; then export KDEV_SHELL_ENVIRONMENT_ID="default" fi if ! [ "$KDEV_DBUS_ID" ]; then echo "The required environment variable KDEV_DBUS_ID is not set. This variable defines the dbus id of the application instance instance which is supposed to be attached." exit 5 fi # Eventually, if we are forwarding to another host, and kdevplatform_shell_environment.sh # has been located through "which kdevplatform_shell_environment.sh", then we need to update KDEV_BASEDIR. if ! [ -e "$KDEV_BASEDIR/kdevplatform_shell_environment.sh" ]; then KDEV_BASEDIR=$(dirname $(which kdevplatform_shell_environment.sh)) fi if ! [ -e "$KDEV_BASEDIR/kdev_dbus_socket_transformer" ]; then echo "The $KDEV_BASEDIR/kdev_dbus_socket_transformer utility is missing, controlling the application across ssh is not possible" fi # Takes a list of tools, and prints a warning of one of them is not available in the path function checkToolsInPath { for TOOL in $@; do if ! [ "$(which $TOOL 2> /dev/null)" ]; then echo "The utility $TOOL is not in your path, the shell integration will not work properly." fi done } # Check if all required tools are there (on the host machine) checkToolsInPath sed qdbus ls cut dirname mktemp basename readlink hostname if ! [ "$KDEV_SSH_FORWARD_CHAIN" ]; then # Check for additional utilities that are required on the client machine - checkToolsInPath kioclient + checkToolsInPath kioclient5 fi # Queries the session name from the running application instance function getSessionName { echo "$(qdbus $KDEV_DBUS_ID /org/kdevelop/SessionController org.kdevelop.SessionController.sessionName)" } function getSessionDir { echo "$(qdbus $KDEV_DBUS_ID /org/kdevelop/SessionController org.kdevelop.SessionController.sessionDir)" } function getCurrentShellEnvPath { local ENV_ID=$KDEV_SHELL_ENVIRONMENT_ID if [ "$1" ]; then ENV_ID=$1 fi echo "$(getSessionDir)/${ENV_ID}.sh" } function help! { echo "You are controlling the $APPLICATION session '$(getSessionName)'" echo "" if [ "$1" == "" ]; then echo "Standard commands:" echo "raise! - Raise the window." echo "sync! - Synchronize the working directory with the currently open document. See \"help! sync\"" echo "open! [file] ... - Open the file(s) within the attached application. See \"help! open\"" echo "eopen! [file] ... - Open the file(s) within an external application using kde-open." echo "create! [file] [[text]] - Create and open a new file." echo "search! [pattern] [[locations]] ... - Search for the given pattern here or at the optionally given location(s)." echo "dsearch! [pattern] [[locations]] ... - Same as search, but starts the search instantly instead of showing the dialog (using previous settings)." echo "ssh! [ssh arguments] - Connect to a remote host via ssh, keeping the control-connection alive. See \"help! remote\"" echo "" echo "help! - Show help." echo "help! open - Show extended help about file opening commands." echo "help! sync - Show extended help about path synchronization commands." echo "help! remote - Show extended help about remote shell-integration through ssh." echo "help! env - Show extended help about the environment." echo "" echo "Most commands can be abbreviated by the first character(s), eg. r! instead of raise!, and se! instead of search!." fi if [ "$1" == "open" ]; then echo "Extended opening:" echo "The open! command can also be used to open files in specific tool-view configurations, by adding split-separators:" echo "- Files around the / separator will be arranged horizontally by split-view." echo "- Files around the - separator will be arranged vertically by split-view." echo "- Parens [ ... ] can be used to disambiguate the hierarchy (there must be spaces between filename and paren)." echo "- If a file is missing around a separator, the currently active view is inserted into the position." echo "" echo "Examples:" echo "open! file1 / file2 - The active view is split horizontally." echo " file1 is opened in the left view, and file2 in the right view." echo "open! file1 / [ file2 - file3 ] - The active view is split horizontally, and the right split-view is split vertically." echo " file1 is opened in the left view, file2 in the right upper view, and file3 in the right lower view." echo "open! / file1 - The active view is split horizontally." echo " - The active document is kept in the left split-view, and file1 is opened in the right split-view." echo "" echo "Short forms: o! = open!, eo! = eopen!, c! = create!" fi if [ "$1" == "sync" ]; then echo "Extended syncing:" echo "sync! [[project-name]] - If no project-name is given, then the sync! command synchronizes to the currently active document." echo " If no document is active, then it synchronizes to the currently selected item in the project tree-view." echo " If a case-insensitive project name prefix is given, then it synchronizes to the base folder of the matching project." echo "syncsel! - Synchronizes to the currently selected item in the project tree-view, independently of the active document." echo "project! [[project-name]] - Map from a path within the build directory to the corresponding path in the source directory." echo " If we're already in the source directory, map to the root of the surrounding project." echo "bdir! [[project-name]] - Map from a path within the source directory to the corresponding path in the build directory." echo " If we're already in the build directory, map to the root of the build directory." echo "" echo "Short forms: s! = sync!, ss! = syncsel!, p! = project!, b! = bdir!" fi if [ "$1" == "remote" ]; then echo "Extended remote commands:" echo "ssh! [ssh arguments] - Connect to a remote host via ssh, keeping the control-connection alive." echo " - The whole dbus environment is forwarded, KDevelop needs to be installed on both sides." echo "ssw! [ssh arguments] - Like ssh!, but preserves the current working directory." echo "exec! [cmd] [args] [file] . .. - Execute the given command on the client machine, referencing any number of local files on the host machine." echo " - The file paths will be re-encoded as fish:// urls if required." echo "cexec! [cmd] [args] [file] . .. - Execute the given command on the client machine, referencing any number of local files on the host machine." echo " - The files will be COPIED to the client machine if required." echo "copytohost! [client path] [host path] - Copy a file/directory through the fish protocol from the client machine th the host machine." echo "copytoclient! [host path] [client path]- Copy a file/directory through the fish protocol from the host machine to the client machine." echo "" echo "Short forms: e! = exec!, ce! = cexec!, cth! = copytohost!, ctc! = copytoclient!" fi if [ "$1" == "env" ]; then echo "Environment management:" echo "The environment can be used to store session-specific macros and generally manipulate the shell environment" echo "for embedded shell sessions. The environment is sourced into the shell when the shell is initialized, and" echo "whenever setenv! is called." echo "" echo "env! - List all available shell environment-ids for this session." echo "setenv! [id] - Set the shell environmnet-id for this session to the given id, or update the current one." echo "editenv! [id] - Edit the current shell environment or the one with the optionally given id." echo "showenv! [id] - Show the current shell environment or the one with the optionally given id." echo "" echo "Short forms: sev! = setenv!, ee! = editenv!, shenv! = showenv!" fi echo "" } # Short versions of the commands: function r! { raise! $@ } function s! { sync! $@ } function ss! { syncsel! } function syncsel! { sync! '[selection]' } function p! { if [ "$@" ]; then s! $@ fi project! } function b! { if [ "$@" ]; then s! $@ fi bdir! } function o! { open! $@ } function eo! { eopen! $@ } function e! { exec! $@ } function ce! { cexec! $@ } function c! { create! $@ } function se! { search! $@ } function ds! { dsearch! $@ } function h! { help! $@ } function cth! { copytohost! $@ } function ctc! { copytoclient! $@ } function sev! { setenv! $@ } function ee! { editenv! $@ } function shev! { showenv! $@ } # Internals: # Opens a document in internally in the application function openDocument { RESULT=$(qdbus $KDEV_DBUS_ID /org/kdevelop/DocumentController org.kdevelop.DocumentController.openDocumentSimple $1) if ! [ "$RESULT" == "true" ]; then echo "Failed to open $1" fi } # Opens a document in internally in the application function openDocuments { RESULT=$(qdbus $KDEV_DBUS_ID /org/kdevelop/DocumentController org.kdevelop.DocumentController.openDocumentsSimple "(" $1 ")") if ! [ "$RESULT" == "true" ]; then echo "Failed to open $1" fi } # Executes a command on the client machine using the custom-script integration. # First argument: The full command. Second argument: The working directory. function executeInApp { local CMD="$1" local WD=$2 if ! [ "$WD" ]; then WD=$(pwd) fi RESULT=$(qdbus $KDEV_DBUS_ID /org/kdevelop/ExternalScriptPlugin org.kdevelop.ExternalScriptPlugin.executeCommand "$CMD" "$WD") if ! [ "$RESULT" == "true" ]; then echo "Execution failed" fi } # First argument: The full command. Second argument: The working directory. # Executes the command silently and synchronously, and returns the output function executeInAppSync { local CMD=$1 local WD=$2 if ! [ "$WD" ]; then WD=$(pwd) fi RESULT=$(qdbus $KDEV_DBUS_ID /org/kdevelop/ExternalScriptPlugin org.kdevelop.ExternalScriptPlugin.executeCommandSync "$CMD" "$WD") echo "$RESULT" } # Getter functions: function getActiveDocument { qdbus $KDEV_DBUS_ID /org/kdevelop/DocumentController org.kdevelop.DocumentController.activeDocumentPath $@ } function getOpenDocuments { qdbus $KDEV_DBUS_ID /org/kdevelop/DocumentController org.kdevelop.DocumentController.activeDocumentPaths } function raise! { qdbus $KDEV_DBUS_ID /kdevelop/MainWindow org.kdevelop.MainWindow.ensureVisible } function bdir! { TARG=$(qdbus $KDEV_DBUS_ID /org/kdevelop/ProjectController org.kdevelop.ProjectController.mapSourceBuild "$(pwd)" false) if [ "$TARG" ]; then cd $TARG else echo "Got no path" fi } function project! { TARG=$(qdbus $KDEV_DBUS_ID /org/kdevelop/ProjectController org.kdevelop.ProjectController.mapSourceBuild "$(pwd)" true) if [ "$TARG" ]; then cd $TARG else echo "Got no path" fi } # Main functions: function raise! { qdbus $KDEV_DBUS_ID /kdevelop/MainWindow org.kdevelop.MainWindow.ensureVisible } function sync! { local P=$(getActiveDocument $@) if [ "$P" ]; then if [[ "$P" == fish://* ]]; then # This regular expression filters the user@host:port out of fish:///user@host:port/path/... LOGIN=$(echo $P | sed "s/fish\:\/\/*\([^\/]*\)\(\/.*\)/\1/") P_ON_HOST=$(echo $P | sed "s/fish\:\/\/*\([^\/]*\)\(\/.*\)/\2/") if [ "$KDEV_SSH_FORWARD_CHAIN" == "$LOGIN" ]; then P="$P_ON_HOST" else if [ "$KDEV_SSH_FORWARD_CHAIN" == "" ]; then # Try to ssh to the host machine # We need to split away the optional ":port" suffix, because the ssh command does not allow that syntax HOST=$(echo $LOGIN | cut --delimiter=':' -f 1) CMD="ssh!" if [[ "$LOGIN" == *:* ]]; then # If there is a port, extract it PORT=$(echo $LOGIN | cut --delimiter=':' -f 2) CMD="$CMD -p $PORT" fi CMD="$CMD $HOST" # Execute the ssh command echo "Executing $CMD" KDEV_WORKING_DIR="$(dirname $P_ON_HOST)" $CMD return else echo "Cannot synchronize the working directory, because the host-names do not match (app: $LOGIN, shell: $KDEV_SSH_FORWARD_CHAIN)" return fi fi elif [[ "$P" == file://* ]]; then P=$(echo $P | sed 's$^file://$$') elif [ "$KDEV_SSH_FORWARD_CHAIN" ]; then # This session is being forwarded to another machine, but the current document is not # However, we won't complain, because it's possible that the machines share the same file-system if [ $(isEqualFileOnHostAndClient $P) != "yes" ]; then echo "Cannot synchronize the working directory, because the file systems do not match" return fi fi [ -d "$P" ] || P=$(dirname "$P") cd "$P" else echo "Got no path" fi } # Take a path, and returns "yes" if the equal file is available on the host and the client # The check is performed by comparing inode-numbers function isEqualFileOnHostAndClient { function trimWhiteSpace() { echo $1 } FILE=$1 INODE_HOST=$(trimWhiteSpace $(ls --color=never -i $FILE | cut -d' ' -f1)) INODE_CLIENT=$(trimWhiteSpace $(executeInAppSync "ls --color=never -i $FILE | cut -d' ' -f1" "$(dirname $FILE)")) if [ "$INODE_HOST" == "$INODE_CLIENT" ]; then echo "yes" else echo "" fi } # Takes a relative file, returns an absolute file/url that should be valid on the client. function mapFileToClient { local RELATIVE_FILE=$1 FILE=$(readlink -f $RELATIVE_FILE) if ! [ -e "$FILE" ]; then # Try opening the file anyway, it might be an url or something else we don't understand here FILE=$RELATIVE_FILE else # We are referencing an absolute file, available on the file-system. if [ "$KDEV_SSH_FORWARD_CHAIN" ]; then # If we are forwarding, map it to the client somehow. if [ "$(isEqualFileOnHostAndClient "$FILE")" != "yes" ]; then # We can eventually map the file using the fish protocol FISH_HOST=$KDEV_SSH_FORWARD_CHAIN if [[ "$FISH_HOST" == *\,* ]]; then # Extracts everything before the first comma FISH_HOST=$(echo $FISH_HOST | sed 's/\([^,]*\),\(.*\)/\1/') echo "ssh chain is too long: $KDEV_SSH_FORWARD_CHAIN mapping anyway using $FISH_HOST" 1>&2 fi # Theoretically, we can only map through fish if the forward-chains contains no comma, which means that # we forward only once. Try anyway, there might be the same filesystem on the whole forward-chain. FILE="fish://$FISH_HOST$FILE" fi fi fi echo $FILE } function open! { FILES=$@ NEWFILES="" for RELATIVE_FILE in $FILES; do if [ "$RELATIVE_FILE" == "/" ]; then FILE=$RELATIVE_FILE else FILE=$(mapFileToClient $RELATIVE_FILE) fi NEWFILES="$NEWFILES $FILE" done openDocuments "$NEWFILES" } function eopen! { FILES=$@ for RELATIVE_FILE in $FILES; do FILE=$(mapFileToClient $RELATIVE_FILE) executeInApp "kde-open $FILE" done } function exec! { FILES=$@ ARGS="" for RELATIVE_FILE in $FILES; do if [ "$ARGS" == "" ]; then # Do not transform the command-name ARGS=$RELATIVE_FILE else FILE=$(mapFileToClient $RELATIVE_FILE) ARGS=$ARGS" "$FILE fi done echo "Executing: " $ARGS executeInApp "$ARGS" } function copytohost! { - executeInApp "kioclient copy $1 $(mapFileToClient $2)" + executeInApp "kioclient5 copy $1 $(mapFileToClient $2)" } function copytoclient! { - executeInApp "kioclient copy $(mapFileToClient $1) $2" + executeInApp "kioclient5 copy $(mapFileToClient $1) $2" } function cexec! { FILES=$@ ARGS="" PREFIX="" TMP=1 for RELATIVE_FILE in $FILES; do if [ "$ARGS" == "" ]; then # Do not transform the command-name ARGS=$RELATIVE_FILE else FILE=$(mapFileToClient $RELATIVE_FILE) if [[ "$FILE" == fish://* ]]; then # Add a prefix to copy the file into a temporary file # Keep the baseline as suffix, so that applications can easily recognize the mimetype - PREFIX+="FILE$TMP=\$(mktemp).$(basename $FILE); kioclient copy $FILE \$FILE$TMP;" + PREFIX+="FILE$TMP=\$(mktemp).$(basename $FILE); kioclient5 copy $FILE \$FILE$TMP;" # Use the temporary variable instead of the name FILE="\$FILE$TMP" TMP=$(($TMP+1)) fi ARGS=$ARGS" "$FILE fi done echo "Executing: " $ARGS executeInApp "$PREFIX $ARGS" } function create! { FILE=$(readlink -f $1) if ! [ "$FILE" ]; then echo "Error: Bad arguments." return 1 fi if [ -e "$FILE" ]; then echo "The file $FILE already exists" return 2 fi echo $2 > $FILE openDocument $(mapFileToClient $FILE) } function search! { PATTERN=$1 # if ! [ "$PATTERN" ]; then # echo "Error: No pattern given." # return 1 # fi LOCATION=$2 if ! [ "$LOCATION" ]; then LOCATION="." fi LOCATION=$(mapFileToClient $LOCATION) for LOC in $*; do if [ "$LOC" == "$1" ]; then continue; fi if [ "$LOC" == "$2" ]; then continue; fi LOCATION="$LOCATION;$(mapFileToClient $LOC)" done qdbus $KDEV_DBUS_ID /org/kdevelop/GrepViewPlugin org.kdevelop.kdevelop.GrepViewPlugin.startSearch "$PATTERN" "$LOCATION" true } function dsearch! { PATTERN=$1 if ! [ "$PATTERN" ]; then echo "Error: No pattern given." return 1 fi LOCATION=$2 if ! [ "$LOCATION" ]; then LOCATION="." fi LOCATION=$(mapFileToClient $LOCATION) for LOC in $*; do if [ "$LOC" == "$1" ]; then continue; fi if [ "$LOC" == "$2" ]; then continue; fi LOCATION="$LOCATION;$(mapFileToClient $LOC)" done qdbus $KDEV_DBUS_ID /org/kdevelop/GrepViewPlugin org.kdevelop.kdevelop.GrepViewPlugin.startSearch "$PATTERN" "$LOCATION" false } ##### SSH DBUS FORWARDING -------------------------------------------------------------------------------------------------------------------- DBUS_SOCKET_TRANSFORMER=$KDEV_BASEDIR/kdev_dbus_socket_transformer # We need this, to make sure that our forwarding-loops won't get out of control # This configures the shell to kill background jobs when it is terminated shopt -s huponexit export DBUS_ABSTRACT_SOCKET_TARGET_BASE_PATH=/tmp/dbus-forwarded-$USER-$APPLICATION_HOST export DBUS_FORWARDING_TCP_LOCAL_PORT=9000 export DBUS_FORWARDING_TCP_MAX_LOCAL_PORT=10000 export DBUS_ABSTRACT_SOCKET_TARGET_INDEX=1 export DBUS_ABSTRACT_SOCKET_MAX_TARGET_INDEX=1000 function getPortFromSSHCommand { # The port is given to ssh exclusively in the format "-p PORT" # This regular expression extracts the "4821" from "ssh -q bla1 -p 4821 bla2" local ARGS=$@ local RET=$(echo "$@" | sed "s/.*-p \+\([0-9]*\).*/\1/") if [ "$ARGS" == "$RET" ]; then # There was no match echo "" else echo ":$RET" fi } function getLoginFromSSHCommand { # The login name can be given to ssh in the format "-l NAME" # This regular expression extracts the "NAME" from "ssh -q bla1 -l NAME bla2" local ARGS=$@ local RET=$(echo "$ARGS" | sed "s/.*-l \+\([a-z,A-Z,_,0-9]*\).*/\1/") if [ "$RET" == "$ARGS" ] || [ "$RET" == "" ]; then # There was no match echo "" else echo "$RET@" fi } function getHostFromSSHCommand { # This regular expression extracts the "bla2" from "echo "ssh -q bla1 -p 4821 bla2" # Specifically, it finds the first argument which is not preceded by a "-x" parameter kind specification. local CLEANED="" local NEWCLEANED="$@" while ! [ "$NEWCLEANED" == "$CLEANED" ]; do CLEANED="$NEWCLEANED" # This expression removes one "-x ARG" parameter NEWCLEANED="$(echo $CLEANED | sed "s/\(.*\)\(-[a-z,A-Z] \+[a-z,0-9]*\)\ \(.*\)/\1\3/")" done # After cleaning, the result should only consist of the host-name followed by an optional command. # Select the host-name, by extracting the forst column. echo $CLEANED | cut --delimiter=" " -f 1 } function getSSHForwardOptionsFromCommand { HOST="$(getLoginFromSSHCommand "$@")$(getHostFromSSHCommand "$@")$(getPortFromSSHCommand "$@")" if [ "$KDEV_SSH_FORWARD_CHAIN" ]; then # We are already forwarding, so we deal with a chain of multiple ssh commands. # We still record it, although it's not sure if we can use it somehow. echo "KDEV_SSH_FORWARD_CHAIN=\"$KDEV_SSH_FORWARD_CHAIN,$HOST\""; else echo "KDEV_SSH_FORWARD_CHAIN=$HOST" fi } function getDBusAbstractSocketSuffix { # From something like DBUS_SESSION_BUS_ADDRESS=unix:abstract=/tmp/dbus-wYmSkVH7FE,guid=b214dad39e0292a4299778d64d761a5b # extract the /tmp/dbus-wYmSkVH7FE echo $DBUS_SESSION_BUS_ADDRESS | sed 's/unix\:abstract\=.*\(,guid\=.*\)/\1/' } function keepForwardingDBusToTCPSocket { while ! $KDEV_BASEDIR/kdev_dbus_socket_transformer $DBUS_FORWARDING_TCP_LOCAL_PORT --bind-only; do if (($DBUS_FORWARDING_TCP_LOCAL_PORT<$DBUS_FORWARDING_TCP_MAX_LOCAL_PORT)); then export DBUS_FORWARDING_TCP_LOCAL_PORT=$(($DBUS_FORWARDING_TCP_LOCAL_PORT+1)) # echo "Increased local port to " $DBUS_FORWARDING_TCP_LOCAL_PORT; else echo "Failed to allocate a local TCP port"; return 1; fi done $KDEV_BASEDIR/kdev_dbus_socket_transformer $DBUS_FORWARDING_TCP_LOCAL_PORT& return 0; } function keepForwardingDBusFromTCPSocket { while ! $KDEV_BASEDIR/kdev_dbus_socket_transformer $FORWARD_DBUS_FROM_PORT ${DBUS_ABSTRACT_SOCKET_TARGET_BASE_PATH}-${DBUS_ABSTRACT_SOCKET_TARGET_INDEX} --bind-only; do if ((${DBUS_ABSTRACT_SOCKET_TARGET_INDEX}<${DBUS_ABSTRACT_SOCKET_MAX_TARGET_INDEX})); then export DBUS_ABSTRACT_SOCKET_TARGET_INDEX=$(($DBUS_ABSTRACT_SOCKET_TARGET_INDEX+1)) else echo "Failed to allocate a local path for the abstract dbus socket"; return 1; fi done local PATH=${DBUS_ABSTRACT_SOCKET_TARGET_BASE_PATH}-${DBUS_ABSTRACT_SOCKET_TARGET_INDEX} export DBUS_SESSION_BUS_ADDRESS=unix:abstract=$PATH${DBUS_SOCKET_SUFFIX} $KDEV_BASEDIR/kdev_dbus_socket_transformer $FORWARD_DBUS_FROM_PORT $PATH& } function ssh! { keepForwardingDBusToTCPSocket # Start the dbus forwarding subprocess DBUS_FORWARDING_TCP_TARGET_PORT=$((5000+($RANDOM%50000))) ssh $@ -t -R localhost:$DBUS_FORWARDING_TCP_TARGET_PORT:localhost:$DBUS_FORWARDING_TCP_LOCAL_PORT \ " APPLICATION=$APPLICATION \ KDEV_BASEDIR=$KDEV_BASEDIR \ KDEV_DBUS_ID=$KDEV_DBUS_ID \ FORWARD_DBUS_FROM_PORT=$DBUS_FORWARDING_TCP_TARGET_PORT \ APPLICATION_HOST=$APPLICATION_HOST \ KDEV_WORKING_DIR=$KDEV_WORKING_DIR \ KDEV_SHELL_ENVIRONMENT_ID=$KDEV_SHELL_ENVIRONMENT_ID \ DBUS_SOCKET_SUFFIX=$(getDBusAbstractSocketSuffix) \ $(getSSHForwardOptionsFromCommand "$@") \ bash --init-file \ \$(if [ -e \"$KDEV_BASEDIR/kdevplatform_shell_environment.sh\" ]; \ then echo \"$KDEV_BASEDIR/kdevplatform_shell_environment.sh\"; \ elif [ -e \"$(which kdevplatform_shell_environment.sh)\" ]; then echo \"$(which kdevplatform_shell_environment.sh)\"; \ else \ echo \"~/.kdevplatform_shell_environment.sh\"; \ fi) \ -i" if [ "$FORWARD_DBUS_FROM_PORT" ]; then # We created the 2nd subprocess kill %2 # Stop the dbus forwarding subprocess else # We created the 1st subprocess kill %1 # Stop the dbus forwarding subprocess fi } # A version of ssh! that preserves the current working directory function ssw! { KDEV_WORKING_DIR=$(pwd) ssh! $@ } function env! { FILES="$(executeInAppSync "ls $(getSessionDir)/*.sh" "")" for FILE in $FILES; do FILE=$(basename $FILE) ID=${FILE%.sh} # This ugly construct strips away the .sh suffix if [ "$ID" == "$KDEV_SHELL_ENVIRONMENT_ID" ]; then echo "$ID [current]" else echo "$ID" fi done } function editenv! { local ENV_ID=$KDEV_SHELL_ENVIRONMENT_ID if [ "$1" ]; then ENV_ID=$1 fi # If the environment-file doesn't exist yet, create it executeInAppSync "if ! [ -e $(getCurrentShellEnvPath $ENV_ID) ]; then touch $(getCurrentShellEnvPath $ENV_ID); fi" "" # Open it openDocument "$(getCurrentShellEnvPath $ENV_ID)" } function setenv! { if [ "$1" ]; then KDEV_SHELL_ENVIRONMENT_ID=$1 fi # Execute the contents of the shell-environment # note: keep compatible with FreeBSD: https://bugs.kde.org/show_bug.cgi?id=311186 local TEMP=$(mktemp /tmp/$USER-XXXXXXXX) RESULT=$(executeInAppSync "cat \"$(getCurrentShellEnvPath)\"" "") echo "$RESULT" > $TEMP if ! [ "$RESULT" ]; then # If the environment shell file doesn't exist, create it executeInAppSync "if ! [ -e $(getCurrentShellEnvPath) ]; then touch $(getCurrentShellEnvPath); fi" "" fi source $TEMP rm $TEMP } function showenv! { local ENV_ID=$KDEV_SHELL_ENVIRONMENT_ID if [ "$1" ]; then ENV_ID=$1 fi echo "Environment $ENV_ID:" # Execute the contents of the shell-environment echo $(executeInAppSync "cat \"$(getCurrentShellEnvPath $ENV_ID)\"" "") } if [ "$FORWARD_DBUS_FROM_PORT" ]; then # Start the target-side dbus forwarding, transforming from the ssh pipe to the abstract unix domain socket export DBUS_SESSION_BUS_ADDRESS=unix:abstract=${DBUS_ABSTRACT_SOCKET_TARGET_BASE_PATH}-${DBUS_ABSTRACT_SOCKET_TARGET_INDEX}${DBUS_SOCKET_SUFFIX} keepForwardingDBusFromTCPSocket fi setenv! ##### INITIALIZATION -------------------------------------------------------------------------------------------------------------------- # Mark that this session is attached, by prepending a '!' character PS1="!$PS1" echo "You are controlling the $APPLICATION session '$(getSessionName)'. Type help! for more information." if [ "$KDEV_WORKING_DIR" ]; then cd $KDEV_WORKING_DIR fi