You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
tdenetwork/kopete/libkopete/kopetepluginmanager.cpp

535 lines
16 KiB

/*
kopetepluginmanager.cpp - Kopete Plugin Loader
Copyright (c) 2002-2003 by Duncan Mac-Vicar Prett <duncan@kde.org>
Copyright (c) 2002-2003 by Martijn Klingens <klingens@kde.org>
Copyright (c) 2002-2004 by Olivier Goffart <ogoffart @tiscalinet.be>
Kopete (c) 2002-2003 by the Kopete developers <kopete-devel@kde.org>
*************************************************************************
* *
* 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 <valgrind/valgrind.h>
#endif
#include <tqapplication.h>
#include <tqfile.h>
#include <tqregexp.h>
#include <tqtimer.h>
#include <tqvaluestack.h>
#include <tdeapplication.h>
#include <kdebug.h>
#include <tdeparts/componentfactory.h>
#include <kplugininfo.h>
#include <ksimpleconfig.h>
#include <kstandarddirs.h>
#include <kstaticdeleter.h>
#include <kurl.h>
#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<KPluginInfo *> plugins;
// Dict of all currently loaded plugins, mapping the KPluginInfo to
// a plugin
typedef TQMap<KPluginInfo *, Plugin *> 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<TQString> pluginsToLoad;
static KStaticDeleter<PluginManager> deleter;
bool isAllPluginsLoaded;
};
KStaticDeleter<PluginManager> 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<KPluginInfo *> PluginManager::availablePlugins( const TQString &category ) const
{
if ( category.isEmpty() )
return d->plugins;
TQValueList<KPluginInfo *> result;
TQValueList<KPluginInfo *>::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, TQ_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<Plugin *>( const_cast<TQObject*>( 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<TQString, bool> pluginsMap;
TQMap<TQString, TQString> entries = config->entryMap( TQString::fromLatin1( "Plugins" ) );
TQMap<TQString, TQString>::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<KPluginInfo *> plugins = availablePlugins( TQString() );
TQValueList<KPluginInfo *>::ConstIterator it2 = plugins.begin();
TQValueList<KPluginInfo *>::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<KPluginInfo *> plugins = availablePlugins( TQString() );
TQValueList<KPluginInfo *>::ConstIterator it = plugins.begin();
TQValueList<KPluginInfo *>::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, TQ_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, TQ_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, TQ_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<Plugin>( 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, TQ_SIGNAL( destroyed( TQObject * ) ), this, TQ_SLOT( slotPluginDestroyed( TQObject * ) ) );
connect( plugin, TQ_SIGNAL( readyForUnload() ), this, TQ_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, TQ_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<KPluginInfo *>::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"