/* kopetepluginmanager.cpp - Kopete Plugin Loader Copyright (c) 2002-2003 by Duncan Mac-Vicar Prett Copyright (c) 2002-2003 by Martijn Klingens Copyright (c) 2002-2004 by Olivier Goffart Kopete (c) 2002-2003 by the Kopete developers ************************************************************************* * * * This library is free software; you can redistribute it and/or * * modify it under the terms of the GNU Lesser General Public * * License as published by the Free Software Foundation; either * * version 2 of the License, or (at your option) any later version. * * * ************************************************************************* */ #include "config.h" #include "kopetepluginmanager.h" #if defined(HAVE_VALGRIND_H) && !defined(NDEBUG) && defined(__i386__) // We don't want the per-skin includes, so pretend we have a skin header already #define __VALGRIND_SOMESKIN_H #include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kopeteplugin.h" #include "kopetecontactlist.h" #include "kopeteaccountmanager.h" namespace Kopete { class PluginManager::Private { public: Private() : shutdownMode( StartingUp ), isAllPluginsLoaded(false) {} // All available plugins, regardless of category, and loaded or not TQValueList plugins; // Dict of all currently loaded plugins, mapping the KPluginInfo to // a plugin typedef TQMap 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 Kopete shutdown by the // async unloading of plugins. enum ShutdownMode { StartingUp, Running, ShuttingDown, DoneShutdown }; ShutdownMode shutdownMode; // Plugins pending for loading TQValueStack pluginsToLoad; static KStaticDeleter deleter; bool isAllPluginsLoaded; }; KStaticDeleter PluginManager::Private::deleter; PluginManager* PluginManager::s_self = 0L; PluginManager* PluginManager::self() { if ( !s_self ) Private::deleter.setObject( s_self, new PluginManager() ); return s_self; } PluginManager::PluginManager() : TQObject( tqApp ), d( new Private ) { d->plugins = KPluginInfo::fromServices( TDETrader::self()->query( TQString::fromLatin1( "Kopete/Plugin" ), TQString::fromLatin1( "[X-Kopete-Version] == 1000900" ) ) ); // We want to add a reference to the application's event loop so we // can remain in control when all windows are removed. // This way we can unload plugins asynchronously, which is more // robust if they are still doing processing. kapp->ref(); } PluginManager::~PluginManager() { if ( d->shutdownMode != Private::DoneShutdown ) kdWarning( 14010 ) << k_funcinfo << "Destructing plugin manager without going through the shutdown process! Backtrace is: " << endl << kdBacktrace() << endl; // Quick cleanup of the remaining plugins, hope it helps // Note that deleting it.data() causes slotPluginDestroyed to be called, which // removes the plugin from the list of loaded plugins. while ( !d->loadedPlugins.empty() ) { Private::InfoToPluginMap::ConstIterator it = d->loadedPlugins.begin(); kdWarning( 14010 ) << k_funcinfo << "Deleting stale plugin '" << it.data()->name() << "'" << endl; delete it.data(); } delete d; } TQValueList PluginManager::availablePlugins( const TQString &category ) const { if ( category.isEmpty() ) return d->plugins; TQValueList result; TQValueList::ConstIterator it; for ( it = d->plugins.begin(); it != d->plugins.end(); ++it ) { if ( ( *it )->category() == category ) result.append( *it ); } return result; } PluginList PluginManager::loadedPlugins( const TQString &category ) const { PluginList result; for ( Private::InfoToPluginMap::ConstIterator it = d->loadedPlugins.begin(); it != d->loadedPlugins.end(); ++it ) { if ( category.isEmpty() || it.key()->category() == category ) result.append( it.data() ); } return result; } KPluginInfo *PluginManager::pluginInfo( const Plugin *plugin ) const { for ( Private::InfoToPluginMap::ConstIterator it = d->loadedPlugins.begin(); it != d->loadedPlugins.end(); ++it ) { if ( it.data() == plugin ) return it.key(); } return 0; } void PluginManager::shutdown() { if(d->shutdownMode != Private::Running) { kdDebug( 14010 ) << k_funcinfo << "called when not running. / state = " << d->shutdownMode << endl; return; } d->shutdownMode = Private::ShuttingDown; /* save the contact list now, just in case a change was made very recently and it hasn't autosaved yet from a OO point of view, theses lines should not be there, but i don't see better place -Olivier */ Kopete::ContactList::self()->save(); Kopete::AccountManager::self()->save(); // Remove any pending plugins to load, we're shutting down now :) d->pluginsToLoad.clear(); // Ask all plugins to unload for ( Private::InfoToPluginMap::ConstIterator it = d->loadedPlugins.begin(); it != d->loadedPlugins.end(); /* EMPTY */ ) { // Plugins could emit their ready for unload signal directly in response to this, // which would invalidate the current iterator. Therefore, we copy the iterator // and increment it beforehand. Private::InfoToPluginMap::ConstIterator current( it ); ++it; // FIXME: a much cleaner approach would be to just delete the plugin now. if it needs // to do some async processing, it can grab a reference to the app itself and create // another object to do it. current.data()->aboutToUnload(); } // When running under valgrind, don't enable the timer because it will almost // certainly fire due to valgrind's much slower processing #if defined(HAVE_VALGRIND_H) && !defined(NDEBUG) && defined(__i386__) if ( RUNNING_ON_VALGRIND ) kdDebug(14010) << k_funcinfo << "Running under valgrind, disabling plugin unload timeout guard" << endl; else #endif TQTimer::singleShot( 3000, this, TQT_SLOT( slotShutdownTimeout() ) ); } void PluginManager::slotPluginReadyForUnload() { // Using TQObject::sender() is on purpose here, because otherwise all // plugins would have to pass 'this' as parameter, which makes the API // less clean for plugin authors // FIXME: I don't buy the above argument. Add a Kopete::Plugin::emitReadyForUnload(void), // and make readyForUnload be passed a plugin. - Richard Plugin *plugin = dynamic_cast( const_cast( sender() ) ); kdDebug( 14010 ) << k_funcinfo << plugin->pluginId() << "ready for unload" << endl; if ( !plugin ) { kdWarning( 14010 ) << k_funcinfo << "Calling object is not a plugin!" << endl; return; } plugin->deleteLater(); } void PluginManager::slotShutdownTimeout() { // When we were already done the timer might still fire. // Do nothing in that case. if ( d->shutdownMode == Private::DoneShutdown ) return; TQStringList remaining; for ( Private::InfoToPluginMap::ConstIterator it = d->loadedPlugins.begin(); it != d->loadedPlugins.end(); ++it ) remaining.append( it.data()->pluginId() ); kdWarning( 14010 ) << k_funcinfo << "Some plugins didn't shutdown in time!" << endl << "Remaining plugins: " << remaining.join( TQString::fromLatin1( ", " ) ) << endl << "Forcing Kopete shutdown now." << endl; slotShutdownDone(); } void PluginManager::slotShutdownDone() { kdDebug( 14010 ) << k_funcinfo << endl; d->shutdownMode = Private::DoneShutdown; kapp->deref(); } void PluginManager::loadAllPlugins() { // FIXME: We need session management here - Martijn TDEConfig *config = TDEGlobal::config(); if ( config->hasGroup( TQString::fromLatin1( "Plugins" ) ) ) { TQMap pluginsMap; TQMap entries = config->entryMap( TQString::fromLatin1( "Plugins" ) ); TQMap::Iterator it; for ( it = entries.begin(); it != entries.end(); ++it ) { TQString key = it.key(); if ( key.endsWith( TQString::fromLatin1( "Enabled" ) ) ) pluginsMap.insert( key.left( key.length() - 7 ), (it.data() == TQString::fromLatin1( "true" )) ); } TQValueList plugins = availablePlugins( TQString() ); TQValueList::ConstIterator it2 = plugins.begin(); TQValueList::ConstIterator end = plugins.end(); for ( ; it2 != end; ++it2 ) { // Protocols are loaded automatically so they aren't always in Plugins group. (fixes bug 167113) if ( (*it2)->category() == TQString::fromLatin1( "Protocols" ) ) continue; TQString pluginName = (*it2)->pluginName(); bool inMap = pluginsMap.contains( pluginName ); if ( (inMap && pluginsMap[pluginName]) || (!inMap && (*it2)->isPluginEnabledByDefault()) ) { if ( !plugin( pluginName ) ) d->pluginsToLoad.push( pluginName ); } else { //This happens if the user unloaded plugins with the config plugin page. // No real need to be assync because the user usualy unload few plugins // compared tto the number of plugin to load in a cold start. - Olivier if ( plugin( pluginName ) ) unloadPlugin( pluginName ); } } } else { // we had no config, so we load any plugins that should be loaded by default. TQValueList plugins = availablePlugins( TQString() ); TQValueList::ConstIterator it = plugins.begin(); TQValueList::ConstIterator end = plugins.end(); for ( ; it != end; ++it ) { if ( (*it)->isPluginEnabledByDefault() ) d->pluginsToLoad.push( (*it)->pluginName() ); } } // Schedule the plugins to load TQTimer::singleShot( 0, this, TQT_SLOT( slotLoadNextPlugin() ) ); } void PluginManager::slotLoadNextPlugin() { if ( d->pluginsToLoad.isEmpty() ) { if ( d->shutdownMode == Private::StartingUp ) { d->shutdownMode = Private::Running; d->isAllPluginsLoaded = true; emit allPluginsLoaded(); } return; } TQString key = d->pluginsToLoad.pop(); loadPluginInternal( key ); // Schedule the next run unconditionally to avoid code duplication on the // allPluginsLoaded() signal's handling. This has the added benefit that // the signal is delayed one event loop, so the accounts are more likely // to be instantiated. TQTimer::singleShot( 0, this, TQT_SLOT( slotLoadNextPlugin() ) ); } Plugin * PluginManager::loadPlugin( const TQString &_pluginId, PluginLoadMode mode /* = LoadSync */ ) { TQString pluginId = _pluginId; // Try to find legacy code // FIXME: Find any cases causing this, remove them, and remove this too - Richard if ( pluginId.endsWith( TQString::fromLatin1( ".desktop" ) ) ) { kdWarning( 14010 ) << "Trying to use old-style API!" << endl << kdBacktrace() << endl; pluginId = pluginId.remove( TQRegExp( TQString::fromLatin1( ".desktop$" ) ) ); } if ( mode == LoadSync ) { return loadPluginInternal( pluginId ); } else { d->pluginsToLoad.push( pluginId ); TQTimer::singleShot( 0, this, TQT_SLOT( slotLoadNextPlugin() ) ); return 0L; } } Plugin *PluginManager::loadPluginInternal( const TQString &pluginId ) { //kdDebug( 14010 ) << k_funcinfo << pluginId << endl; KPluginInfo *info = infoForPluginId( pluginId ); if ( !info ) { kdWarning( 14010 ) << k_funcinfo << "Unable to find a plugin named '" << pluginId << "'!" << endl; return 0L; } if ( d->loadedPlugins.contains( info ) ) return d->loadedPlugins[ info ]; int error = 0; Plugin *plugin = KParts::ComponentFactory::createInstanceFromQuery( TQString::fromLatin1( "Kopete/Plugin" ), TQString::fromLatin1( "[X-TDE-PluginInfo-Name]=='%1'" ).arg( pluginId ), this, 0, TQStringList(), &error ); if ( plugin ) { d->loadedPlugins.insert( info, plugin ); info->setPluginEnabled( true ); connect( plugin, TQT_SIGNAL( destroyed( TQObject * ) ), this, TQT_SLOT( slotPluginDestroyed( TQObject * ) ) ); connect( plugin, TQT_SIGNAL( readyForUnload() ), this, TQT_SLOT( slotPluginReadyForUnload() ) ); kdDebug( 14010 ) << k_funcinfo << "Successfully loaded plugin '" << pluginId << "'" << endl; emit pluginLoaded( plugin ); } else { switch( error ) { case KParts::ComponentFactory::ErrNoServiceFound: kdDebug( 14010 ) << k_funcinfo << "No service implementing the given mimetype " << "and fullfilling the given constraint expression can be found." << endl; break; case KParts::ComponentFactory::ErrServiceProvidesNoLibrary: kdDebug( 14010 ) << "the specified service provides no shared library." << endl; break; case KParts::ComponentFactory::ErrNoLibrary: kdDebug( 14010 ) << "the specified library could not be loaded." << endl; break; case KParts::ComponentFactory::ErrNoFactory: kdDebug( 14010 ) << "the library does not export a factory for creating components." << endl; break; case KParts::ComponentFactory::ErrNoComponent: kdDebug( 14010 ) << "the factory does not support creating components of the specified type." << endl; break; } kdDebug( 14010 ) << k_funcinfo << "Loading plugin '" << pluginId << "' failed, KLibLoader reported error: '" << endl << KLibLoader::self()->lastErrorMessage() << "'" << endl; } return plugin; } bool PluginManager::unloadPlugin( const TQString &spec ) { //kdDebug(14010) << k_funcinfo << spec << endl; if( Plugin *thePlugin = plugin( spec ) ) { thePlugin->aboutToUnload(); return true; } else return false; } void PluginManager::slotPluginDestroyed( TQObject *plugin ) { for ( Private::InfoToPluginMap::Iterator it = d->loadedPlugins.begin(); it != d->loadedPlugins.end(); ++it ) { if ( it.data() == plugin ) { d->loadedPlugins.erase( it ); break; } } if ( d->shutdownMode == Private::ShuttingDown && d->loadedPlugins.isEmpty() ) { // Use a timer to make sure any pending deleteLater() calls have // been handled first TQTimer::singleShot( 0, this, TQT_SLOT( slotShutdownDone() ) ); } } Plugin* PluginManager::plugin( const TQString &_pluginId ) const { // Hack for compatibility with Plugin::pluginId(), which returns // classname() instead of the internal name. Changing that is not easy // as it invalidates the config file, the contact list, and most likely // other code as well. // For now, just transform FooProtocol to kopete_foo. // FIXME: In the future we'll need to change this nevertheless to unify // the handling - Martijn TQString pluginId = _pluginId; if ( pluginId.endsWith( TQString::fromLatin1( "Protocol" ) ) ) pluginId = TQString::fromLatin1( "kopete_" ) + _pluginId.lower().remove( TQString::fromLatin1( "protocol" ) ); // End hack KPluginInfo *info = infoForPluginId( pluginId ); if ( !info ) return 0L; if ( d->loadedPlugins.contains( info ) ) return d->loadedPlugins[ info ]; else return 0L; } KPluginInfo * PluginManager::infoForPluginId( const TQString &pluginId ) const { TQValueList::ConstIterator it; for ( it = d->plugins.begin(); it != d->plugins.end(); ++it ) { if ( ( *it )->pluginName() == pluginId ) return *it; } return 0L; } bool PluginManager::setPluginEnabled( const TQString &_pluginId, bool enabled /* = true */ ) { TQString pluginId = _pluginId; TDEConfig *config = TDEGlobal::config(); config->setGroup( "Plugins" ); // FIXME: What is this for? This sort of thing is tdeconf_update's job - Richard if ( !pluginId.startsWith( TQString::fromLatin1( "kopete_" ) ) ) pluginId.prepend( TQString::fromLatin1( "kopete_" ) ); if ( !infoForPluginId( pluginId ) ) return false; config->writeEntry( pluginId + TQString::fromLatin1( "Enabled" ), enabled ); config->sync(); return true; } bool PluginManager::isAllPluginsLoaded() const { return d->isAllPluginsLoaded; } } //END namespace Kopete #include "kopetepluginmanager.moc"