New plugin API
Gary Kramlich
grim at reaperworld.com
Fri Aug 9 01:57:28 EDT 2013
I'll speak to the gplugin questions as I haven't been following Ankit's
changes for purple directly.
In GPlugin, a plugin does *NOT* need to implement a single GObject to do
anything. A GPluginPlugin just gives a developer access to run code from a
loadable module. Internally, the query, load, and unload functions in the
plugin are stored in a GPluginNativePlugin GObject, but there is no good
way to create a subclass of GPluginPlugin that could be loaded by any
loader unless you use a decorator object that contains the GPluginLoader's
GPluginPlugin subclass.
If we were to have the protocols subclass the plugin, then the loader needs
to know what type to register so that it can instantiate it and create it.
That also means that a plugin can only contain one protocol. The way it
stands right now, we could do something crazy like have the IRC plugin add
the base IRC protocol, and then add additional protocol's that have
predefined servers and stuff for popular networks. This example seems a
bit over zealous, but think of the XMPP situation. The XMPP plugin could
register XMPP, Google Talk, Facebook XMPP (is this still around?), etc.
But the library will also let you do that in separate plugins as well.
I think the real problem here is how we both interpret ProtocolPlugin. To
me, it's a plugin that implements a protocol, and it would seem to me, that
your interpretation is that's it's a protocol that *IS* a plugin.
The ABI version in the GPluginPluginInfo object is for what version of the
GPlugin ABI you're working against, it doesn't reflect on pidgin as well.
The PURPLE_PLUGIN_MAGIC stuff, should probably be implemented in
PurplePluginInfo to require 3.1.0, 3.0.1, etc.
For subclassing, I don't know how Ankit implemented it, but the way GPlugin
was designed to support, was that you create a GObject that subclasses the
other object. Therefore you need access to it's headers to compile. But
it's treated like any other subclass in the GType system.
For dependent plugin loading, I wrote that a long time ago, but if my
memory serves (that's a big if), the plugin that's depended on is loaded
first, if it fails to load, so does it's dependent.
Plugin category is a good idea and I might add that right into GPlugin
propper. It is just a text field and can be translated. "category" is the
property name and can't be translated, but it's value can be whatever.
For the _with_(base)?name stuff. GPlugin uses id's rather than generic
names. There is no enforcement of following the standard in GPlugin, but
the convention is library/application_name-plugin_name. For example
"gplugin-python-plugin-loader", where "gplugin" is the
library/application_name and "python-plugin-loader" is the plugin name.
One thing I wanted to add that Ankit didn't touch on is why use id's over
names and how they're implemented. Being a general purpose library, there
are quite a few more cases I have to worry about with GPlugin. One of them
is people installing plugins into the wrong location. But using the above
naming convention, it makes it easier while debugging or looking at
gplugin-query (more on this in a minute) to figure out what's going on.
Aside from that, I'm sure most of us have helped a user that ended up
having a "buggy" plugin that wasn't updating because they installed a
version a long time ago to ~/.purple/plugins and then later installed it
into /usr/lib/purple-2. In libpurple right now, the last one seen is the
one that's used. GPlugin allows you to "see" both, and in fact even load
both versions at the same time. This was mostly modeled after the Netscape
Plugin implementation. For example, according to my chrome://plugins I
have 3 versions of Java installed right now, and while I'm in "details"
mode I can enable and disable specific versions.
gplugin-query is a command line tool that will allow you to query plugins
you have installed. For example:
grim at cloak:~$ gplugin-query -ivv
gplugin-python-loader
name: Python Plugin Loader
version: 0.0.4dev
summary: A plugin that can load python plugins
author: Gary Kramlich <grim at reaperworld.com>
website: http://bitbucket.org/rw_grim/gplugin
filename: /usr/local/lib/gplugin/gplugin-python.so
abi version: 1
flags: load-on-query,internal
loader: GPluginNativePluginLoader
description: This plugin allows the load of plugins written in the python
programming language.
I only have the python loader installed right now, but we have all the
information we have in a plugin dialog right now in a quick and easy to
read format. It also supports adding additional paths, so a simple script
could be created to have it query the libpurple/pidgin plugin directories
as well.
If you have any more questions about GPlugin feel free ;)
Thanks,
--
Gary Kramlich <grim at reaperworld.com>
On Thu, Aug 8, 2013 at 9:08 PM, Eion Robb <eion at robbmob.com> wrote:
>
> I like where a lot of this is going :)
>
>
>
>
> I have a few questions/comments though:
>
> I think that protocols should extend from PurplePlugin still... most of
> that stuff in the PurplePluginInfo object is needed in a protocol,
> including dependencies, names, icons, versions etc and to duplicate that in
> a PurplePluginProtocolInfo would be yucky. Or maybe I'm missing something
> about the encapsulating thing you were talking about... a PurplePlugin
> contains PurpleProtocolPluginInfo's? If so, are they free'd when the
> plugin is unloaded?
>
> What's "PURPLE_ABI_VERSION"? If I wanted to say my plugin works on
> version 3.0.0 onwards, but I'm building on 3.2.1 source, what value would I
> use?
>
> If I wanted to sub-class a protocol (e.g. make a 'new' xmpp-based
> protocol), how would I do so in the new model?
>
> Are GPLUGIN_PLUGIN_INFO_FLAGS_INTERNAL and
> GPLUGIN_PLUGIN_INFO_FLAGS_LOAD_ON_QUERY actually part of GPlugin, or are
> they part of libpurple (and should thusly be renamed)?
>
> Your comment about "A plugin is deemed to not be loadable if..." seems to
> be a bit light on details, eg "The ABI version requirement for the plugin
> does not match" match what? Are we moving away from the
> backwards-compatible thing, or did you mean to say that the ABI version has
> to be less-than-or-equal-to what the UI has been compiled with?
> What happens if two plugins with the same id's try to be loaded at the
> same time?
>
> If a plugin depends on another one, is that one loaded first, if it hasn't
> yet been loaded? Are we also able to specify a version of plugin that we
> depend on?
>
> Should the plugin "category" property be translated, or are they id's that
> the UI translates?
>
> Why have purple_plugins_find_with_name() and
> purple_plugins_find_with_basename() been removed? A quick google search
> shows that they're being used by UI's and plugins.
>
>
> That's it for now :)
>
> Cheers,
> Eion
>
>
> On 9 August 2013 07:37, Ankit Vani <a at nevitus.org> wrote:
>
>> Hi everyone.
>>
>> I have been working on the plugin API for over two weeks now, and I feel
>> I am in
>> a good place with where it stands now to talk about it and ask for
>> suggestions.
>> There are things that I'm still not quite sure of, and things that could
>> be done
>> better. I would appreciate suggestions to make this implementation of the
>> plugins API better.
>>
>> Repo: http://hg.pidgin.im/soc/2013/ankitkv/gobjectification
>> Branch: soc.2013.gobjectification.plugins
>> Files of interest: libpurple/plugins.[ch]
>>
>> ---
>>
>> INTRODUCTION
>> ============
>>
>> One of the goals of my Google Summer of Code proposals was the
>> introduction of
>> GObject-based plugins. For this, I decided to start with a new API, that
>> would
>> provide the ability to register new types in the GObject type system, and
>> would
>> provide higher flexibility without sacrificing simplicity for libpurple
>> and
>> plugin authors. Due to the problems in the current plugin API, such as the
>> difficulty in writing a loaded loader or a loaded protocol due to the
>> differentiation between protocols, loaders, and standard plugins, I
>> decided not
>> to just add GObjectification to the current plugin API. Free language
>> loaders
>> was also an interesting prospect of this change.
>>
>> As it turns out, Gary (grim) had already written a very promising plugin
>> library
>> that pretty much did exactly what we want, GPlugin
>> (https://bitbucket.org/rw_grim/gplugin). It is robust, lets you register
>> new
>> types into the GObject type system, and can support plugins written in
>> different
>> programming languages via loaders (although I believe only native C is
>> fully
>> functional as of yet). Evaluating all the requirements for the new plugin
>> API
>> with Ethan, it seemed that if I was to not use GPlugin, I would have
>> ended up
>> with an API whose functionality resembled that of GPlugin anyway, but
>> instead
>> would've taken longer to write. So rather than spending time reinventing
>> the
>> wheel, we decided to use GPlugin as the backbone for the new purple
>> plugin API.
>>
>>
>> BRIEF OVERVIEW OF GPLUGIN
>> =========================
>>
>> A brief introduction to the basic data structures of GPlugin:
>>
>> 1. GPluginPlugin
>> This is an abstract type that represents a plugin of any type in
>> GPlugin.
>>
>> 2. GPluginNativePlugin
>> It inherits GPluginPlugin and implements the GTypePlugin interface of
>> the
>> GObject library. It represents a native C plugin. It allows
>> registering new
>> types and interfaces into the type system.
>>
>> 3. GPluginPluginLoader
>> It is an abstract type that represents a plugin loader. The plugin
>> loader is
>> responsible for loading plugins of a particular language.
>>
>> 4. GPluginNativePluginLoader
>> It is the loader for native C plugins. This is where the actual module
>> loading, unloading, and querying happens for native plugins.
>>
>> 5. GPluginPluginInfo
>> This type holds the information about a plugin, such as the id, name,
>> author,
>> website etc. This is a GObject instead of a struct, so that
>> applications such
>> as libpurple can inherit it and add their own fields in the plugin's
>> info.
>>
>> The gplugin_plugin_manager_* API provides the calls to add search paths,
>> finding
>> plugins, and loading/unloading of plugins. You don't really need to care
>> much
>> about loaders as GPlugin pretty much handles them transparently, unless
>> you're
>> writing a loader in which case you can register your loader using
>> gplugin_plugin_manager_register_loader().
>>
>> Here is an example of a very basic plugin that you can write using
>> GPlugin:
>>
>> https://bitbucket.org/rw_grim/gplugin/src/f64b9822d56dd25da027c26e47319f6817dffb0b/tests/plugins/basic-plugin.c?at=default
>>
>> A plugin requires three functions:
>> 1. gplugin_plugin_query() returns a new plugin info object that specifies
>> things
>> like id, name, author etc. for the plugin.
>> 2. gplugin_plugin_load(plugin) runs code on load and returns TRUE to
>> indicate a
>> successful load, or FALSE to cancel the load.
>> 3. gplugin_plugin_unload(plugin) runs code on unload and returns TRUE if
>> unload
>> succeeded, otherwise FALSE.
>>
>>
>> GPLUGIN WITH LIBPURPLE
>> ======================
>>
>> GPlugin has been added as an external dependency for libpurple. If
>> --disable-plugins parameter is provided to configure, GPlugin is not
>> required
>> and all dynamic loading magic is disabled. Of course, protocols can still
>> be
>> linked statically in this case.
>>
>> Since GPluginPlugin represents a plugin, one way to integrate GPlugin with
>> libpurple would be using GPluginPlugin instead of PurplePlugin throughout
>> the
>> codebase. We would use the GPlugin API directly, and for purple-related
>> plugin
>> stuff, we'd use a purple API. However, there are a number of problems
>> with this
>> approach:
>>
>> 1. When --disable-plugins is provided, we want to disable the entire
>> dynamic
>> loading mechanism, and GPlugin should no longer be required. This is a
>> problem when there are calls to the GPlugin API and types such as
>> GPluginPlugin and GPluginPluginInfo all around the codebase.
>> 2. It would be really confusing for developers and plugin authors, when
>> some of
>> the things needed a GPlugin API and some things needed a purple API.
>> Moreover, at times you would need to cast purple stuff to GPlugin
>> stuff,
>> which gets ugly soon enough.
>> 3. Memory management becomes a problem with needing to remember to unref
>> returned GPlugin objects and lists all the time, since the rest of the
>> GObjectified libpurple does not ref and unref unless necessary. Forget
>> an
>> unref once somewhere, and you'd have spend time and energy hunting
>> down the
>> leak.
>> 4. It becomes difficult to document everything, because not everything is
>> in
>> libpurple. And the documentation in libpurple would at times have to
>> assume
>> familiarity with the documentation of GPlugin.
>>
>> Thus, the solution I chose was to keep everything simple and purple, and
>> only
>> deal with GPlugin in plugins.[ch], where the purple plugin API is
>> implemented.
>> Basically, the purple plugin API wraps the GPlugin stuff wherever needed.
>>
>> When plugins support is enabled, gplugin.h is included and all GPlugin
>> calls are
>> enabled.
>> In this case, PurplePluginInfo inherits GPluginPluginInfo to hold
>> additional
>> purple information for a plugin like the UI requirement, actions, whether
>> the
>> plugin is loadable, preference frame callback etc.
>> PurplePlugin is a typedef for GPluginPlugin, and represents a plugin of
>> any
>> type.
>>
>> When plugins support is disabled, GPlugin is not linked, gplugin.h is not
>> included, and all GPlugin stuff is disabled by an #ifdef PURPLE_PLUGINS
>> guard.
>> In this case, PurplePluginInfo inherits GObject, and isn't really going
>> to be
>> instantiated anywhere.
>> PurplePlugin is a typedef for GObject, and again, isn't going to be
>> instantiatd
>> anywhere (protocols are no longer represented by their plugins, I get to
>> that in
>> the last section).
>>
>> A GPlugin plugin requires the three functions I mentioned in the previous
>> section. However, due to the different semantics of loading and unloading
>> when a
>> plugin is to be linked dynamically or statically, a macro that does the
>> appropriate work is introduced.
>>
>> This macro is:
>> PURPLE_PLUGIN_INIT(plugin-name, query-func, load-func, unload-func)
>>
>> When linking dynamically, this expands to the
>> gplugin_plugin_[query,load,unload]
>> functions.
>> When linking statically, this expands to pluginname_plugin_[load,unload]
>> functions, called by static_proto_load() and
>> static_proto_unload().
>>
>>
>> PURPLE PLUGIN API
>> =================
>>
>> One of the main changes in the new plugin API is that there is no
>> distinction of
>> plugins based on whether they're protocols, loaders or standard plugins.
>> You
>> could write a standard plugin that provides a new protocol, or multiple
>> protocols, or a loader, or both, etc (not saying that you should do them
>> crazy
>> stuffs). But this actually simplifies a lot of things, while giving plugin
>> authors more flexibility.
>>
>> I will discuss the aspects that have changed in this API, and leave out
>> the
>> obvious stuff.
>>
>> ---
>>
>> Plugin info
>> -----------
>>
>> Object hierarchy:
>>
>> GObject
>> +----GPluginPluginInfo
>> +----PurplePluginInfo
>> +----FinchPluginInfo
>> +----PidginPluginInfo
>>
>> PurplePluginInfo inherits GPluginPluginInfo and holds information about a
>> plugin. An instance of PurplePluginInfo has to be returned from the
>> plugin's
>> query function, by providing the appropriate values. See the example at
>> the end
>> of this section.
>>
>> A plugin info instance is created by calling
>> purple_plugin_info_new(prop1-name, prop1-value, prop2-name, prop2-value,
>> ...);
>>
>> Valid properties are:
>>
>> 1. "id" (string): The ID of the plugin.
>> 2. "name" (string): The name of the plugin.
>> 3. "version" (string): Version of the plugin.
>> 4. "category" (string): Primary category of the plugin.
>> 5. "summary" (string): Summary of the plugin.
>> 6. "description" (string): Description of the plugin.
>> 7. "author" (string): Author of the plugin.
>> 8. "website" (string): Website of the plugin.
>> 9. "icon" (string): Path to a plugin's icon.
>> 10. "license" (string): The plugin's license.
>> 11. "abi_version" (guint32): The required ABI version for the
>> plugin.
>> 12. "dependencies" (GSList): List of plugin IDs required by the
>> plugin.
>> 13. "preferences_frame" (PurplePluginPrefFrameCallback): Callback that
>> returns
>> a preferences frame for the
>> plugin.
>>
>> All properties except "id" and "abi_version" are optional, and may not be
>> specified.
>>
>> A "flags" property is also present. I talk about it in a subsequent
>> subsection.
>>
>> - Since we also have a GSoC project that is working on a new plugins
>> window, I
>> have temporarily added a property "category", which could organize
>> plugins in
>> the plugins window.
>> - "icon", which is a GPluginPluginInfo property may also be helpful in
>> some way
>> in the new plugins window.
>> - I'm not sure if we need a "license" property, which is also a
>> GPluginPluginInfo property.
>>
>> Please let me know if I should remove any of these properties, since every
>> property has an appropriate get function in the API, for example
>> purple_plugin_info_get_name(), purple_plugin_info_get_author() etc.
>>
>> An additional property "ui_requirement" is also present, but it should
>> not be
>> set manually.
>> It is set if you create a plugin info instance using one of these
>> functions
>> instead of purple_plugin_info_new():
>>
>> 1. finch_plugin_info_new(): (in gntplugin) Creates a finch plugin, with
>> UI
>> requirement as finch. A property
>> "finch_preferences_frame" can be set to a
>> callback
>> that creates a GNT frame.
>> 1. pidgin_plugin_info_new(): (in gtkplugin) Creates a pidgin plugin, with
>> UI
>> requirement as pidgin. A property
>> "pidgin_config_frame" can be set to a
>> callback that
>> creates a GTK frame.
>>
>> The PurplePluginInfo instance for a plugin can be retrieved by calling
>> purple_plugin_get_info(plugin).
>>
>>
>> Plugin requirements
>> -------------------
>>
>> A plugin that does not meet all the requirements is not loadable. To
>> check if a
>> plugin is loadable, call purple_plugin_is_loadable(plugin).
>> If it is not loadable, purple_plugin_get_error(plugin) should give you the
>> reason why not.
>>
>> A plugin is deemed to not be loadable if:
>> 1. It does not have an ID.
>> 2. The UI requirement for the plugin does not match.
>> 3. The ABI version requirement for the plugin does not match.
>>
>> This reason can be displayed by the UI, but attempts to load the plugin
>> will
>> fail.
>>
>> A plugin is also not loadable if it does not return an info instance.
>> However,
>> purple_plugin_get_error() will return NULL in this case because there
>> would be
>> nowhere to store the error.
>>
>>
>> Internal and load-on-query plugins
>> ----------------------------------
>>
>> A plugin can also specify a bitwise combination of two values as a "flags"
>> property. These values are:
>> 1. GPLUGIN_PLUGIN_INFO_FLAGS_INTERNAL: Internal plugin.
>> 2. GPLUGIN_PLUGIN_INFO_FLAGS_LOAD_ON_QUERY: Auto-load on query.
>>
>> - Internal plugins are plugins that should not be shown in plugin lists
>> of the
>> UI.
>> - Load-on-query plugins are automatically loaded when
>> purple_plugins_refresh()
>> is called. Such plugins are not saved by purple_plugins_save_loaded().
>> Once
>> unloaded, a load-on-query plugin is not auto-loaded until the next
>> restart.
>>
>> Protocol plugins are to be both internal and load-on-query.
>>
>>
>> Actions
>> -------
>>
>> Till now, the actions field was a callback, that returned a GList of
>> actions
>> that had to be instantiated and added to the list.
>> Now, actions can be added using
>> purple_plugin_add_action(plugin, label, callback) in the plugin's load
>> function,
>> without plugin authors needing to create a PurplePluginAction instance and
>> adding it to a list.
>>
>> Use purple_plugin_get_actions(plugin) to get a list of PurplePluginAction
>> instances of that plugin. Unlike before, this list is owned by the plugin
>> info
>> and should not be freed.
>>
>>
>> Registering new types
>> ---------------------
>>
>> GPlugin provides these functions to register a new type with the GObject
>> type
>> system:
>> 1. GType gplugin_native_plugin_register_type(GPluginNativePlugin *plugin,
>> GType parent, const gchar *name,
>> const GTypeInfo *info,
>> GTypeFlags flags);
>> 2. void gplugin_native_plugin_add_interface(GPluginNativePlugin *plugin,
>> GType instance_type,
>> GType interface_type,
>> const GInterfaceInfo
>> *interface_info);
>> 3. GType gplugin_native_plugin_register_enum(GPluginNativePlugin *plugin,
>> const gchar *name,
>> const GEnumValue *values);
>> 4. GType gplugin_native_plugin_register_flags(GPluginNativePlugin *plugin,
>> const gchar *name,
>> const GFlagsValue *values);
>>
>> You can use these functions directly, however, for simplicity,
>> uniformity, and
>> to avoid plugin authors from having to cast PurplePlugin to
>> GPluginNativePlugin,
>> I have added these functions in the purple API to wrap the functions 1
>> and 2
>> above:
>> 1. GType purple_plugin_register_type(PurplePlugin *plugin, GType parent,
>> const gchar *name, const GTypeInfo
>> *info,
>> GTypeFlags flags);
>> 2. void purple_plugin_add_interface(PurplePlugin *plugin, GType
>> instance_type,
>> GType interface_type,
>> const GInterfaceInfo
>> *interface_info);
>>
>> When more languages are supported, these functions can provide a
>> transparent
>> interface to register new types.
>>
>>
>> Removed API
>> -----------
>>
>> - IPC has not been implemented yet. I'm not sure if we need it - do we?
>> Would
>> signals suffice?
>> - purple_plugins_find_with_name() and purple_plugins_find_with_basename()
>> have
>> no equivalents in the new API. Plugins can still be looked up by id or
>> filename.
>>
>>
>> A basic plugin example
>> ----------------------
>>
>> /*
>> * This example does pretty much the same thing as the helloworld plugin
>> in
>> * the libpurple plugins, without the comments.
>> */
>>
>> #include "plugins.h"
>>
>> static void
>> plugin_action_test_cb(PurplePluginAction *action)
>> {
>> purple_notify_message(action->plugin, PURPLE_NOTIFY_MSG_INFO,
>> "Plugin Actions Test",
>> "This is a plugin actions test :)", NULL, NULL,
>> NULL);
>> }
>>
>> static PurplePluginInfo *
>> plugin_query(void)
>> {
>> return purple_plugin_info_new(
>> "id", "core-example",
>> "name", "Example plugin",
>> "version", "1.0",
>> "summary", "An example plugin",
>> "description", "An example plugin",
>> "abi_version", PURPLE_ABI_VERSION,
>> NULL
>> );
>> }
>>
>> static gboolean
>> plugin_load(PurplePlugin *plugin)
>> {
>> purple_plugin_add_action(plugin, "Action Test",
>> plugin_action_test_cb);
>>
>> purple_notify_message(plugin, PURPLE_NOTIFY_MSG_INFO, "Hello World!",
>> "This is the example plugin :)", NULL, NULL,
>> NULL);
>>
>> return TRUE;
>> }
>>
>> static gboolean
>> plugin_unload(PurplePlugin *plugin)
>> {
>> return TRUE;
>> }
>>
>> PURPLE_PLUGIN_INIT(example, plugin_query, plugin_load, plugin_unload);
>>
>> ---
>>
>> Please take a look at plugins.h and plugins.c in my repository to get the
>> full
>> picture.
>>
>>
>> PROTOCOLS (TEMPORARY FIX)
>> =========================
>>
>> Since a plugin cannot represent a protocol anymore (as a prpl), a
>> protocol has
>> to have its own representation that can be added and removed from the
>> global
>> protocols list by plugins. This is temporarily done by
>> PurplePluginProtocolInfo,
>> with three extra fields (id, name, actions). The way protocols are
>> defined will
>> probably be going through some changes as well after the plugin stuff is
>> all
>> done. But for now, this is the solution to get working protocols with the
>> new
>> plugin API without modifying the protocol interface (much) yet.
>>
>> For now, for example, this simplifies this:
>> PurplePlugin *prpl;
>> PurplePluginProtocolInfo *prpl_info;
>> prpl = purple_connection_get_prpl(gc);
>> prpl_info = PURPLE_PLUGIN_PROTOCOL_INFO(prpl);
>>
>> to this:
>> PurplePluginProtocolInfo *prpl_info;
>> prpl_info = purple_connection_get_protocol_info(gc);
>>
>> To add a new protocol, a plugin has to call
>> purple_protocols_add(prpl_info).
>> To remove it, call purple_protocols_remove(prpl_info).
>> Removing a protocol disconnects all connected accounts using that
>> particular
>> protocol, and frees its user splits, protocol options and actions.
>>
>> ---
>>
>> I will now start refactoring existing prpls and plugins to use the new
>> plugin
>> API. However, I want to ensure everything is as per what the community
>> wants
>> before I go all out doing so. Please suggest changes and improvements.
>>
>> I will post an update about any major changes done henceforth.
>>
>>
>> - Ankit
>>
>> _______________________________________________
>> Devel mailing list
>> Devel at pidgin.im
>> http://pidgin.im/cgi-bin/mailman/listinfo/devel
>>
>
>
> _______________________________________________
> Devel mailing list
> Devel at pidgin.im
> http://pidgin.im/cgi-bin/mailman/listinfo/devel
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://pidgin.im/pipermail/devel/attachments/20130809/c7ca1fce/attachment-0001.html>
More information about the Devel
mailing list