// -*- c-basic-offset: 3 -*- /* This file is part of the KDE libraries * Copyright (C) 1999 David Faure * Copyright (C) 2002-2003 Waldo Bastian * * 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 "kbuildsycoca.h" #include "ksycoca_p.h" #include "ksycocaresourcelist.h" #include "vfolder_menu.h" #include #include #include #include "kbuildservicetypefactory.h" #include "kbuildmimetypefactory.h" #include "kbuildservicefactory.h" #include "kbuildservicegroupfactory.h" #include "kbuildprotocolinfofactory.h" #include "kctimefactory.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include typedef QHash KBSEntryDict; typedef QList KSycocaEntryListList; static quint32 newTimestamp = 0; static KBuildServiceFactory *g_serviceFactory = 0; static KBuildServiceGroupFactory *g_buildServiceGroupFactory = 0; static KSycocaFactory *g_currentFactory = 0; static KCTimeInfo *g_ctimeInfo = 0; // factory static KCTimeDict *g_ctimeDict = 0; // old timestamps static QByteArray g_resource = 0; static KBSEntryDict *g_currentEntryDict = 0; static KBSEntryDict *g_serviceGroupEntryDict = 0; static KSycocaEntryListList *g_allEntries = 0; // entries from existing ksycoca static QStringList *g_allResourceDirs = 0; static bool g_changed = false; static KSycocaEntry::List g_tempStorage; static VFolderMenu *g_vfolder = 0; static QByteArray g_sycocaPath = 0; static QLatin1String g_defaultMenu = QLatin1String("kde-applications.menu"); static bool bGlobalDatabase = false; static bool bMenuTest = false; void crashHandler(int sig) { KDE_signal(sig, SIG_DFL); // If we crash while reading sycoca, we delete the database in an attempt to recover. if (!g_sycocaPath.isEmpty()) { unlink(g_sycocaPath.constData()); } ::exit(sig); } static QString sycocaPath() { return KSycoca::absoluteFilePath(bGlobalDatabase ? KSycoca::GlobalDatabase : KSycoca::LocalDatabase); } KBuildSycoca::KBuildSycoca() : KSycoca( true ) { } KBuildSycoca::~KBuildSycoca() { } KSycocaEntry::Ptr KBuildSycoca::createEntry(const QString &file, bool addToFactory) { quint32 timeStamp = g_ctimeInfo->dict()->ctime(file, g_resource); if (!timeStamp) { timeStamp = KGlobal::dirs()->calcResourceHash( g_resource, file, KStandardDirs::Recursive); } KSycocaEntry::Ptr entry; if (g_allEntries) { assert(g_ctimeDict); quint32 oldTimestamp = g_ctimeDict->ctime(file, g_resource); if (timeStamp && (timeStamp == oldTimestamp)) { // Re-use old entry if (g_currentFactory == g_buildServiceGroupFactory) // Strip .directory from service-group entries { entry = g_currentEntryDict->value(file.left(file.length()-10)); } else { entry = g_currentEntryDict->value(file); } // remove from g_ctimeDict; if g_ctimeDict is not empty // after all files have been processed, it means // some files were removed since last time if (file.contains("fake")) kDebug(7021) << g_resource << "reusing (and removing) old entry [\"fake\"] for:" << file; g_ctimeDict->remove(file, g_resource); } else if (oldTimestamp) { g_changed = true; g_ctimeDict->remove(file, g_resource); kDebug(7021) << "modified:" << file << "in" << g_resource.constData(); } else { g_changed = true; kDebug(7021) << "new:" << file << "in" << g_resource.constData(); } } g_ctimeInfo->dict()->addCTime(file, g_resource, timeStamp); if (!entry) { // Create a new entry entry = g_currentFactory->createEntry( file, g_resource ); } if ( entry && entry->isValid() ) { if (addToFactory) g_currentFactory->addEntry(entry); else g_tempStorage.append(entry); return entry; } return KSycocaEntry::Ptr(); } KService::Ptr KBuildSycoca::createService(const QString &path) { KSycocaEntry::Ptr entry = createEntry(path, false); return KService::Ptr::staticCast(entry); } // returns false if the database is up to date, true if it needs to be saved bool KBuildSycoca::build() { typedef QList KBSEntryDictList; KBSEntryDictList entryDictList; KBSEntryDict *serviceEntryDict = 0; // Convert for each factory the entryList to a Dict. int i = 0; // For each factory for (KSycocaFactoryList::Iterator factory = factories()->begin(); factory != factories()->end(); ++factory) { KBSEntryDict *entryDict = new KBSEntryDict; if (g_allEntries) { const KSycocaEntry::List list = (*g_allEntries)[i++]; for( KSycocaEntry::List::const_iterator it = list.begin(); it != list.end(); ++it) { entryDict->insert( (*it)->entryPath(), *it ); } } if ((*factory) == g_serviceFactory) serviceEntryDict = entryDict; else if ((*factory) == g_buildServiceGroupFactory) g_serviceGroupEntryDict = entryDict; entryDictList.append(entryDict); } QStringList allResources; // we could use QSet - does order matter? // For each factory for (KSycocaFactoryList::Iterator factory = factories()->begin(); factory != factories()->end(); ++factory) { // For each resource the factory deals with const KSycocaResourceList *list = (*factory)->resourceList(); if (!list) continue; for( KSycocaResourceList::ConstIterator it1 = list->constBegin(); it1 != list->constEnd(); ++it1 ) { KSycocaResource res = (*it1); if (!allResources.contains(res.resource)) allResources.append(res.resource); } } g_ctimeInfo = new KCTimeInfo(); // This is a build factory too, don't delete!! bool uptodate = true; // For all resources foreach(const QString &it1, allResources) { g_changed = false; g_resource = it1.toLatin1(); QStringList relFiles; (void) KGlobal::dirs()->findAllResources( g_resource, QString(), KStandardDirs::Recursive | KStandardDirs::NoDuplicates, relFiles); // Now find all factories that use this resource.... // For each factory KBSEntryDictList::const_iterator ed_it = entryDictList.begin(); const KBSEntryDictList::const_iterator ed_end = entryDictList.end(); KSycocaFactoryList::const_iterator it = factories()->constBegin(); const KSycocaFactoryList::const_iterator end = factories()->constEnd(); for ( ; it != end; ++it, ++ed_it ) { g_currentFactory = (*it); // g_ctimeInfo gets created after the initial loop, so it has no entryDict. g_currentEntryDict = ed_it == ed_end ? 0 : *ed_it; // For each resource the factory deals with const KSycocaResourceList *list = g_currentFactory->resourceList(); if (!list) continue; for( KSycocaResourceList::ConstIterator it2 = list->constBegin(); it2 != list->constEnd(); ++it2 ) { KSycocaResource res = (*it2); if (res.resource != it1) continue; // For each file in the resource foreach(const QString &it3, relFiles) { // Check if file matches filter if (it3.endsWith(res.extension)) createEntry(it3, true); } } } if (g_changed || !g_allEntries) { uptodate = false; //kDebug() << "CHANGED:" << g_resource; m_changedResources.append(g_resource); } } bool result = !uptodate || (g_ctimeDict && !g_ctimeDict->isEmpty()); if (g_ctimeDict && !g_ctimeDict->isEmpty()) { //kDebug() << "Still in time dict:"; //g_ctimeDict->dump(); // ## It seems entries filtered out by vfolder are still in there, // so we end up always saving ksycoca, i.e. this method never returns false // Get the list of resources from which some files were deleted const QStringList resources = g_ctimeDict->resourceList(); kDebug() << "Still in the time dict (i.e. deleted files)" << resources; m_changedResources += resources; } if (result || bMenuTest) { g_resource = "xdgdata-apps"; g_currentFactory = g_serviceFactory; g_currentEntryDict = serviceEntryDict; g_changed = false; g_vfolder = new VFolderMenu(g_serviceFactory, this); if (!m_trackId.isEmpty()) g_vfolder->setTrackId(m_trackId); QString applicationsMenu = g_defaultMenu; const QString xdgMenuPrefix = QString::fromLocal8Bit(qgetenv("XDG_MENU_PREFIX")); if (!xdgMenuPrefix.isEmpty()) { applicationsMenu = xdgMenuPrefix + QLatin1String("applications.menu"); const bool isValid = !KStandardDirs::locate("xdgconf-menu", applicationsMenu).isEmpty(); if (!isValid) { kWarning() << "Specified XDG_MENU_PREFIX results in invalid menu" << applicationsMenu; applicationsMenu = g_defaultMenu; } } VFolderMenu::SubMenu *kdeMenu = g_vfolder->parseMenu(applicationsMenu); KServiceGroup::Ptr entry = g_buildServiceGroupFactory->addNew("/", kdeMenu->directoryFile, KServiceGroup::Ptr(), false); entry->setLayoutInfo(kdeMenu->layoutList); createMenu(QString(), QString(), kdeMenu); (void) existingResourceDirs(); *g_allResourceDirs += g_vfolder->allDirectories(); if (g_changed || !g_allEntries) { uptodate = false; //kDebug() << "CHANGED:" << g_resource; m_changedResources.append(g_resource); } if (bMenuTest) { result = false; } } qDeleteAll(entryDictList); return result; } void KBuildSycoca::createMenu(const QString &caption_, const QString &name_, VFolderMenu::SubMenu *menu) { QString caption = caption_; QString name = name_; foreach (VFolderMenu::SubMenu *subMenu, menu->subMenus) { QString subName = name+subMenu->name+'/'; QString directoryFile = subMenu->directoryFile; if (directoryFile.isEmpty()) directoryFile = subName+".directory"; quint32 timeStamp = g_ctimeInfo->dict()->ctime(directoryFile, g_resource); if (!timeStamp) { timeStamp = KGlobal::dirs()->calcResourceHash( g_resource, directoryFile, KStandardDirs::Recursive ); } KServiceGroup::Ptr entry; if (g_allEntries) { const quint32 oldTimestamp = g_ctimeDict->ctime(directoryFile, g_resource); if (timeStamp && (timeStamp == oldTimestamp)) { KSycocaEntry::Ptr group = g_serviceGroupEntryDict->value(subName); if ( group ) { entry = KServiceGroup::Ptr::staticCast( group ); if (entry->directoryEntryPath() != directoryFile) entry = 0; // Can't reuse this one! } } } g_ctimeInfo->dict()->addCTime(directoryFile, g_resource, timeStamp); entry = g_buildServiceGroupFactory->addNew(subName, subMenu->directoryFile, entry, subMenu->isDeleted); entry->setLayoutInfo(subMenu->layoutList); if (! (bMenuTest && entry->noDisplay()) ) createMenu(caption + entry->caption() + '/', subName, subMenu); } if (caption.isEmpty()) caption += '/'; if (name.isEmpty()) name += '/'; foreach (const KService::Ptr &p, menu->items) { if (bMenuTest) { if (!menu->isDeleted && !p->noDisplay()) printf("%s\t%s\t%s\n", qPrintable( caption ), qPrintable( p->menuId() ), qPrintable( KStandardDirs::locate("xdgdata-apps", p->entryPath() ) ) ); } else { g_buildServiceGroupFactory->addNewEntryTo( name, p ); } } } bool KBuildSycoca::recreate() { QString path(sycocaPath()); // KSaveFile first writes to a temp file. // Upon finalize() it moves the stuff to the right place. KSaveFile database(path); bool openedOK = database.open(); if (!openedOK && database.error() == QFile::PermissionsError && QFile::exists(path)) { QFile::remove( path ); openedOK = database.open(); } if (!openedOK) { fprintf(stderr, "kbuildsycoca4: ERROR creating database '%s'! %s\n", path.toLocal8Bit().data(), database.errorString().toLocal8Bit().data()); return false; } QDataStream* str = new QDataStream(&database); kDebug(7021).nospace() << "Recreating ksycoca file (" << path << ", version " << KSycoca::version() << ")"; // It is very important to build the servicetype one first // Both are registered in KSycoca, no need to keep the pointers KSycocaFactory *stf = new KBuildServiceTypeFactory; KBuildMimeTypeFactory* mimeTypeFactory = new KBuildMimeTypeFactory; g_buildServiceGroupFactory = new KBuildServiceGroupFactory(); g_serviceFactory = new KBuildServiceFactory(stf, mimeTypeFactory, g_buildServiceGroupFactory); (void) new KBuildProtocolInfoFactory(); if( build()) // Parse dirs { save(str); // Save database if (str->status() != QDataStream::Ok) // ######## TODO: does this detect write errors, e.g. disk full? database.abort(); // Error delete str; str = 0; if (!database.finalize()) { fprintf(stderr, "kbuildsycoca4: ERROR writing database '%s'!\n", database.fileName().toLocal8Bit().data()); fprintf(stderr, "kbuildsycoca4: Disk full?\n"); return false; } } else { delete str; str = 0; database.abort(); if (bMenuTest) return true; kDebug(7021) << "Database is up to date"; } if (!bGlobalDatabase) { // update the timestamp file QString stamppath = path + "stamp"; QFile ksycocastamp(stamppath); ksycocastamp.open( QIODevice::WriteOnly ); QDataStream str( &ksycocastamp ); str << newTimestamp; str << existingResourceDirs(); if (g_vfolder) str << g_vfolder->allDirectories(); // Extra resource dirs } return true; } void KBuildSycoca::save(QDataStream* str) { // Write header (#pass 1) str->device()->seek(0); (*str) << (qint32) KSycoca::version(); KBuildServiceFactory * serviceFactory = 0; for(KSycocaFactoryList::Iterator factory = factories()->begin(); factory != factories()->end(); ++factory) { qint32 aId; qint32 aOffset; aId = (*factory)->factoryId(); if ( aId == KST_KServiceFactory ) serviceFactory = static_cast( *factory ); aOffset = (*factory)->offset(); // not set yet, so always 0 (*str) << aId; (*str) << aOffset; } (*str) << (qint32) 0; // No more factories. // Write KDEDIRS (*str) << KGlobal::dirs()->kfsstnd_prefixes(); (*str) << newTimestamp; (*str) << KGlobal::locale()->language(); (*str) << KGlobal::dirs()->calcResourceHash("services", "update_ksycoca", KStandardDirs::Recursive ); (*str) << (*g_allResourceDirs); // Calculate per-servicetype/mimetype data serviceFactory->postProcessServices(); // Here so that it's the last debug message kDebug(7021) << "Saving"; // Write factory data.... for(KSycocaFactoryList::Iterator factory = factories()->begin(); factory != factories()->end(); ++factory) { (*factory)->save(*str); if (str->status() != QDataStream::Ok) // ######## TODO: does this detect write errors, e.g. disk full? return; // error } int endOfData = str->device()->pos(); // Write header (#pass 2) str->device()->seek(0); (*str) << (qint32) KSycoca::version(); for(KSycocaFactoryList::Iterator factory = factories()->begin(); factory != factories()->end(); ++factory) { qint32 aId; qint32 aOffset; aId = (*factory)->factoryId(); aOffset = (*factory)->offset(); (*str) << aId; (*str) << aOffset; } (*str) << (qint32) 0; // No more factories. // Jump to end of database str->device()->seek(endOfData); } bool KBuildSycoca::checkDirTimestamps( const QString& dirname, const QDateTime& stamp, bool top ) { if( top ) { QFileInfo inf( dirname ); if( inf.lastModified() > stamp ) { kDebug( 7021 ) << "timestamp changed:" << dirname; return false; } } QDir dir( dirname ); const QFileInfoList list = dir.entryInfoList( QDir::NoFilter, QDir::Unsorted ); if (list.isEmpty()) return true; foreach ( const QFileInfo& fi, list ) { if( fi.fileName() == "." || fi.fileName() == ".." ) continue; if( fi.lastModified() > stamp ) { kDebug( 7021 ) << "timestamp changed:" << fi.filePath(); return false; } if( fi.isDir() && !checkDirTimestamps( fi.filePath(), stamp, false )) return false; } return true; } // check times of last modification of all files on which ksycoca depens, // and also their directories // if all of them are older than the timestamp in file ksycocastamp, this // means that there's no need to rebuild ksycoca bool KBuildSycoca::checkTimestamps( quint32 timestamp, const QStringList &dirs ) { kDebug( 7021 ) << "checking file timestamps"; QDateTime stamp; stamp.setTime_t( timestamp ); foreach(const QString &it, dirs) { if( !checkDirTimestamps( it, stamp, true )) return false; } kDebug( 7021 ) << "timestamps check ok"; return true; } QStringList KBuildSycoca::existingResourceDirs() { static QStringList* dirs = NULL; if( dirs != NULL ) return *dirs; dirs = new QStringList; g_allResourceDirs = new QStringList; // these are all resources cached by ksycoca QStringList resources; resources += KBuildServiceTypeFactory::resourceTypes(); resources += KBuildMimeTypeFactory::resourceTypes(); resources += KBuildServiceGroupFactory::resourceTypes(); resources += KBuildServiceFactory::resourceTypes(); resources += KBuildProtocolInfoFactory::resourceTypes(); while( !resources.empty()) { QString res = resources.front(); *dirs += KGlobal::dirs()->resourceDirs( res.toLatin1()); resources.removeAll( res ); } *g_allResourceDirs = *dirs; for( QStringList::Iterator it = dirs->begin(); it != dirs->end(); ) { QFileInfo inf( *it ); if( !inf.exists() || !inf.isReadable() ) it = dirs->erase( it ); else ++it; } return *dirs; } static const char appFullName[] = "org.kde.kbuildsycoca"; static const char appName[] = "kbuildsycoca4"; static const char appVersion[] = "1.1"; int main(int argc, char **argv) { KAboutData d(appName, "kdelibs4", ki18n("KBuildSycoca"), appVersion, ki18n("Rebuilds the system configuration cache."), KAboutData::License_GPL, ki18n("(c) 1999-2002 KDE Developers")); d.addAuthor(ki18n("David Faure"), ki18n("Author"), "faure@kde.org"); d.addAuthor(ki18n("Waldo Bastian"), ki18n("Author"), "bastian@kde.org"); KCmdLineOptions options; options.add("nosignal", ki18n("Do not signal applications to update")); options.add("noincremental", ki18n("Disable incremental update, re-read everything")); options.add("checkstamps", ki18n("Check file timestamps")); options.add("nocheckfiles", ki18n("Disable checking files (dangerous)")); options.add("global", ki18n("Create global database")); options.add("menutest", ki18n("Perform menu generation test run only")); options.add("track ", ki18n("Track menu id for debug purposes")); KCmdLineArgs::init(argc, argv, &d); KCmdLineArgs::addCmdLineOptions(options); KCmdLineArgs *args = KCmdLineArgs::parsedArgs(); bGlobalDatabase = args->isSet("global"); bMenuTest = args->isSet("menutest"); if (bGlobalDatabase) { setenv("KDEHOME", "-", 1); setenv("KDEROOTHOME", "-", 1); } QCoreApplication k(argc, argv); KComponentData mainComponent(d); KCrash::setCrashHandler(crashHandler); // force generating of KLocale object. if not, the database will get // be translated KGlobal::locale(); KGlobal::dirs()->addResourceType("app-reg", 0, "share/application-registry" ); while(QDBusConnection::sessionBus().isConnected()) { // kapp registered already, but with the PID in the name. // We need to re-register without it, to detect already-running kbuildsycoca instances. if (QDBusConnection::sessionBus().interface()->registerService(appFullName, QDBusConnectionInterface::QueueService) != QDBusConnectionInterface::ServiceQueued) { break; // Go } fprintf(stderr, "Waiting for already running %s to finish.\n", appName); QEventLoop eventLoop; QObject::connect(QDBusConnection::sessionBus().interface(), SIGNAL(serviceRegistered(QString)), &eventLoop, SLOT(quit())); eventLoop.exec( QEventLoop::ExcludeUserInputEvents ); } fprintf(stderr, "%s running...\n", appName); bool checkfiles = bGlobalDatabase || args->isSet("checkfiles"); bool incremental = !bGlobalDatabase && args->isSet("incremental") && checkfiles; if (incremental || !checkfiles) { KSycoca::disableAutoRebuild(); // Prevent deadlock QString current_language = KGlobal::locale()->language(); QString ksycoca_language = KSycoca::self()->language(); quint32 current_update_sig = KGlobal::dirs()->calcResourceHash("services", "update_ksycoca", KStandardDirs::Recursive ); quint32 ksycoca_update_sig = KSycoca::self()->updateSignature(); QString current_prefixes = KGlobal::dirs()->kfsstnd_prefixes(); QString ksycoca_prefixes = KSycoca::self()->kfsstnd_prefixes(); if ((current_update_sig != ksycoca_update_sig) || (current_language != ksycoca_language) || (current_prefixes != ksycoca_prefixes) || (KSycoca::self()->timeStamp() == 0)) { incremental = false; checkfiles = true; KBuildSycoca::clearCaches(); } } bool checkstamps = incremental && args->isSet("checkstamps") && checkfiles; quint32 filestamp = 0; QStringList oldresourcedirs; if( checkstamps && incremental ) { QString path = sycocaPath()+"stamp"; g_sycocaPath = QFile::encodeName(path); // Delete timestamps on crash QFile ksycocastamp(path); if( ksycocastamp.open( QIODevice::ReadOnly )) { QDataStream str( &ksycocastamp ); if (!str.atEnd()) str >> filestamp; if (!str.atEnd()) { str >> oldresourcedirs; if( oldresourcedirs != KBuildSycoca::existingResourceDirs()) checkstamps = false; } else { checkstamps = false; } if (!str.atEnd()) { QStringList extraResourceDirs; str >> extraResourceDirs; oldresourcedirs += extraResourceDirs; } } else { checkstamps = false; } g_sycocaPath.clear(); } newTimestamp = (quint32) time(0); QStringList changedResources; if( checkfiles && ( !checkstamps || !KBuildSycoca::checkTimestamps( filestamp, oldresourcedirs ))) { g_sycocaPath = QFile::encodeName(sycocaPath()); g_allEntries = 0; g_ctimeDict = 0; if (incremental) { kDebug(7021) << "Reusing existing ksycoca"; KSycoca::self(); KSycocaFactoryList *factories = new KSycocaFactoryList; g_allEntries = new KSycocaEntryListList; g_ctimeDict = new KCTimeDict; // Must be in same order as in KBuildSycoca::recreate()! factories->append( new KServiceTypeFactory ); factories->append( new KMimeTypeFactory ); factories->append( new KServiceGroupFactory ); factories->append( new KServiceFactory ); factories->append( new KProtocolInfoFactory ); // For each factory for (KSycocaFactoryList::Iterator factory = factories->begin(); factory != factories->end(); ++factory) { const KSycocaEntry::List list = (*factory)->allEntries(); g_allEntries->append( list ); } delete factories; factories = 0; KCTimeInfo *ctimeInfo = new KCTimeInfo; *g_ctimeDict = ctimeInfo->loadDict(); } g_sycocaPath.clear(); KBuildSycoca *sycoca = new KBuildSycoca; // Build data base (deletes oldSycoca) if (args->isSet("track")) sycoca->setTrackId(args->getOption("track")); if (!sycoca->recreate()) { return -1; } changedResources = sycoca->changedResources(); if (bGlobalDatabase) { // These directories may have been created with 0700 permission // better delete them if they are empty QString servicetypesDir = KGlobal::dirs()->saveLocation("servicetypes", QString(), false); ::rmdir(QFile::encodeName(servicetypesDir)); } } if (args->isSet("signal")) { // Notify ALL applications that have a ksycoca object, using a signal changedResources.removeDuplicates(); QDBusMessage signal = QDBusMessage::createSignal("/", "org.kde.KSycoca", "notifyDatabaseChanged" ); signal << changedResources; if (QDBusConnection::sessionBus().isConnected()) { kDebug() << "Emitting notifyDatabaseChanged" << changedResources; QDBusConnection::sessionBus().send(signal); qApp->processEvents(); // make sure the dbus signal is sent before we quit. } } return 0; } #include "moc_kbuildsycoca.cpp"