cdebconf plugins (custom widgets)
Hi,
So, I had a long plane journey to LCA, and decided it was about time to
write up a design for custom widgets in cdebconf. For those who haven't
heard talk about this before, this has long been considered a blocker
for a high-quality graphical installer (since the default set of widgets
are really too limiting in that context); it's also potentially
interesting in the newt frontend for things like a partition manager
that isn't just constructed out of select lists, and so on.
I've attached two files: one is a document explaining the design (which
I think is about the most straightforward one possible), and another is
a diff with a working implementation. As a random example, a "help"
widget for the gtk frontend might look like this:
#include "common.h"
#include "frontend.h"
#include "question.h"
#include <gtk/gtk.h>
int gtk_handler_help(struct frontend *obj, struct question *q, GtkWidget *qbox)
{
GtkWidget *label;
label = gtk_label_new(question_get_field(q, "", "extended_description"));
gtk_label_set_line_wrap(GTK_LABEL(label), TRUE);
gtk_box_pack_start(GTK_BOX(qbox), label, FALSE, FALSE, 5);
return DC_OK;
}
There are a few open questions remaining:
* With this design, udeb code is likely to look something like "if
this all-dancing widget is supported, then use it; otherwise, do it
by hand pretty much the way it looks at the moment". My sample
implementation exports the list of supported plugins as "plugin-foo"
capabilities in the CAPB response, but I wonder if a new SUPPORTS
command (so you could do "db_supports whizzy-new-widget") mightn't
be better. On the other hand, that's more client-side divergence
from debconf, and maybe a simple /bin/debconf-supports command would
be simpler.
* I envisage custom widgets being fairly complex in some cases, with
several items of data being returned at once: for example, consider
a combined language/country chooser widget that dynamically updates
the list of countries presented as you change the selected language,
or something like that. It's hard (and arguably a bad idea) to put
much structured data in a single question, though. I don't have a
clear answer to this at the moment; maybe the right approach is for
a complex widget to register extra templates for itself, and just
put the answers there, so that 'db_input foo/whizzy' gives you
answers to foo/whizzy/language and foo/whizzy/country, or something
like that.
Cheers,
--
Colin Watson [cjwatson@debian.org]
cdebconf plugins (custom widgets)
---------------------------------
cdebconf has the facility to load custom implementations for template types
at run-time. This may be used to provide more sophisticated user interfaces
than can be achieved using the standard generic template types. Naturally,
these implementations are per-frontend.
The CAPB command may be used to query the existence of a given plugin for
the current frontend. If the type "foo" is supported by a plugin, CAPB will
return the capability "plugin-foo".
Plugins are dynamically-loaded libraries. The library must be in the
following location:
<frontend_path>/<frontend>/plugin-<type>.so
(The initial "plugin-" is to allow for more convenient testing within the
cdebconf source tree.)
In the default cdebconf.conf, <frontend_path> is /usr/lib/cdebconf/frontend.
Thus, an implementation of the "detect-keyboard" type for the newt frontend
would be stored in:
/usr/lib/cdebconf/frontend/newt/plugin-detect-keyboard.so
The library must provide a function with the following name:
<frontend>_handler_<type>
Any hyphens in <type> here are substituted with underscores, so:
newt_handler_detect_keyboard
The required prototype for this function depends on the frontend. For newt
and text, it is (again with hyphens in <type> substituted with underscores):
int <frontend>_handler_<type>(struct frontend *obj, struct question *q)
For gtk, it is:
int <frontend>_handler_<type>(struct frontend *obj, struct question *q, GtkWidget *questionbox);
Index: doc/plugins.txt
===================================================================
--- doc/plugins.txt (revision 0)
+++ doc/plugins.txt (revision 0)
@@ -0,0 +1,42 @@
+cdebconf plugins (custom widgets)
+---------------------------------
+
+cdebconf has the facility to load custom implementations for template types
+at run-time. This may be used to provide more sophisticated user interfaces
+than can be achieved using the standard generic template types. Naturally,
+these implementations are per-frontend.
+
+The CAPB command may be used to query the existence of a given plugin for
+the current frontend. If the type "foo" is supported by a plugin, CAPB will
+return the capability "plugin-foo".
+
+Plugins are dynamically-loaded libraries. The library must be in the
+following location:
+
+ <frontend_path>/<frontend>/plugin-<type>.so
+
+(The initial "plugin-" is to allow for more convenient testing within the
+cdebconf source tree.)
+
+In the default cdebconf.conf, <frontend_path> is /usr/lib/cdebconf/frontend.
+Thus, an implementation of the "detect-keyboard" type for the newt frontend
+would be stored in:
+
+ /usr/lib/cdebconf/frontend/newt/plugin-detect-keyboard.so
+
+The library must provide a function with the following name:
+
+ <frontend>_handler_<type>
+
+Any hyphens in <type> here are substituted with underscores, so:
+
+ newt_handler_detect_keyboard
+
+The required prototype for this function depends on the frontend. For newt
+and text, it is (again with hyphens in <type> substituted with underscores):
+
+ int <frontend>_handler_<type>(struct frontend *obj, struct question *q)
+
+For gtk, it is:
+
+ int <frontend>_handler_<type>(struct frontend *obj, struct question *q, GtkWidget *questionbox);
Index: src/Makefile.in
===================================================================
--- src/Makefile.in (revision 26632)
+++ src/Makefile.in (working copy)
@@ -19,7 +19,7 @@
LIBOBJS=commands.opic configuration.opic confmodule.opic debug.opic \
database.opic debconfclient.opic frontend.opic priority.opic \
- strutl.opic question.opic template.opic rfc822.opic
+ strutl.opic question.opic template.opic rfc822.opic plugin.opic
CLILIBOBJS=debconfclient.opic
Index: src/commands.c
===================================================================
--- src/commands.c (revision 26693)
+++ src/commands.c (working copy)
@@ -4,6 +4,7 @@
#include "database.h"
#include "question.h"
#include "template.h"
+#include "plugin.h"
#include "strutl.h"
#include <dlfcn.h>
@@ -112,6 +113,7 @@
int i;
char *argv[32];
int argc;
+ char *plugin_capb;
char *out;
argc = strcmdsplit(arg, argv, DIM(argv));
@@ -121,8 +123,11 @@
for (i = 0; i < argc; i++)
if (strcmp(argv[i], "backup") == 0)
mod->frontend->capability |= DCF_CAPB_BACKUP;
+ plugin_capb = plugin_db_capabilities(mod->frontend->plugins);
- asprintf(&out, "%u multiselect backup", CMDSTATUS_SUCCESS);
+ asprintf(&out, "%u multiselect backup%s%s", CMDSTATUS_SUCCESS,
+ *plugin_capb ? " " : "", plugin_capb);
+ free(plugin_capb);
return out;
}
Index: src/frontend.c
===================================================================
--- src/frontend.c (revision 26668)
+++ src/frontend.c (working copy)
@@ -1,5 +1,6 @@
#include "common.h"
#include "configuration.h"
+#include "plugin.h"
#include "database.h"
#include "frontend.h"
#include "question.h"
@@ -177,7 +178,9 @@
snprintf(obj->configpath, sizeof(obj->configpath),
"frontend::instance::%s", modname);
+ obj->plugins = plugin_db_initialize(cfg, modname);
+
#define SETMETHOD(method) if (obj->methods.method == NULL) obj->methods.method = frontend_##method
SETMETHOD(initialize);
Index: src/frontend.h
===================================================================
--- src/frontend.h (revision 26668)
+++ src/frontend.h (working copy)
@@ -15,6 +15,7 @@
struct question_db;
struct question;
struct frontend;
+struct plugin_db;
#define DCF_CAPB_BACKUP (1UL << 0)
@@ -64,6 +65,8 @@
/* methods */
struct frontend_module methods;
+ /* plugin handles */
+ struct plugin_db *plugins;
};
struct frontend *frontend_new(struct configuration *, struct template_db *, struct question_db *);
Index: src/modules/frontend/text/text.c
===================================================================
--- src/modules/frontend/text/text.c (revision 26632)
+++ src/modules/frontend/text/text.c (working copy)
@@ -44,6 +44,7 @@
#include "question.h"
#include "frontend.h"
#include "database.h"
+#include "plugin.h"
#include "strutl.h"
#include <ctype.h>
@@ -768,6 +769,8 @@
{ "error", text_handler_error },
};
+struct question_handlers *all_question_handlers;
+
/*
* Function: text_initialize
* Input: struct frontend *obj - frontend UI object
@@ -781,7 +784,26 @@
*/
static int text_initialize(struct frontend *obj, struct configuration *conf)
{
+ int i, questions;
+
obj->interactive = 1;
+
+ questions =
+ sizeof(question_handlers) / sizeof(struct question_handlers);
+ all_question_handlers =
+ malloc((questions + obj->plugins->n_plugins + 1) *
+ sizeof(struct question_handlers));
+ memcpy(all_question_handlers, question_handlers,
+ sizeof(question_handlers));
+ for (i = 0; i < obj->plugins->n_plugins; ++i) {
+ all_question_handlers[questions + i].type =
+ obj->plugins->plugins[i]->name;
+ all_question_handlers[questions + i].handler =
+ obj->plugins->plugins[i]->handler;
+ }
+ all_question_handlers[questions + i].type = NULL;
+ all_question_handlers[questions + i].handler = NULL;
+
signal(SIGINT, SIG_IGN);
return DC_OK;
}
@@ -810,13 +832,13 @@
static int text_go(struct frontend *obj)
{
struct question *q = obj->questions;
- int i;
+ struct question_handlers *qhandler;
int ret = DC_OK;
int display_title = 1;
while (q != NULL) {
- for (i = 0; i < DIM(question_handlers); i++) {
- if (strcmp(q->template->type, question_handlers[i].type) == 0)
+ for (qhandler = all_question_handlers; qhandler->type; ++qhandler) {
+ if (strcmp(q->template->type, qhandler->type) == 0)
{
if (display_title)
@@ -832,7 +854,7 @@
display_title = 0;
}
text_handler_displaydesc(obj, q);
- ret = question_handlers[i].handler(obj, q);
+ ret = qhandler->handler(obj, q);
if (ret == DC_OK)
obj->qdb->methods.set(obj->qdb, q);
else if (ret == DC_GOBACK && q->prev != NULL)
Index: src/modules/frontend/gtk/gtk.c
===================================================================
--- src/modules/frontend/gtk/gtk.c (revision 26632)
+++ src/modules/frontend/gtk/gtk.c (working copy)
@@ -49,6 +49,7 @@
#include "question.h"
#include "frontend.h"
#include "database.h"
+#include "plugin.h"
#include "strutl.h"
#include "cdebconf_gtk.h"
@@ -759,6 +760,8 @@
{ "text", gtkhandler_text }
};
+struct question_handlers *all_question_handlers;
+
void set_window_properties(GtkWidget *window)
{
gtk_widget_set_size_request (window, 600, 400);
@@ -819,6 +822,7 @@
GtkWidget *window;
int args = 1;
char **name;
+ int questions, i;
//FIXME: This can surely be done in a better way
(char**) name = malloc(2 * sizeof(char*));
@@ -829,6 +833,20 @@
obj->data = NEW(struct frontend_data);
obj->interactive = 1;
+ questions = sizeof(question_handlers) / sizeof(struct question_handlers);
+ all_question_handlers =
+ malloc((questions + obj->plugins->n_plugins + 1) *
+ sizeof(struct question_handlers));
+ memcpy(all_question_handlers, question_handlers, sizeof(question_handlers));
+ for (i = 0; i < obj->plugins->n_plugins; ++i) {
+ all_question_handlers[questions + i].type =
+ obj->plugins->plugins[i]->name;
+ all_question_handlers[questions + i].handler =
+ obj->plugins->plugins[i]->handler;
+ }
+ all_question_handlers[questions + i].type = NULL;
+ all_question_handlers[questions + i].handler = NULL;
+
gtk_init (&args, &name);
window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
@@ -844,7 +862,7 @@
{
struct frontend_data *data = (struct frontend_data *) obj->data;
struct question *q = obj->questions;
- int i;
+ struct question_handlers *qhandler;
int ret;
GtkWidget *questionbox;
@@ -862,10 +880,10 @@
*/
while (q != 0)
{
- for (i = 0; i < DIM(question_handlers); i++)
- if (strcmp(q->template->type, question_handlers[i].type) == 0)
+ for (qhandler = all_question_handlers; qhandler->type; ++qhandler)
+ if (strcmp(q->template->type, qhandler->type) == 0)
{
- ret = question_handlers[i].handler(obj, q, questionbox);
+ ret = qhandler->handler(obj, q, questionbox);
if (ret != DC_OK)
{
return ret;
Index: src/modules/frontend/newt/newt.c
===================================================================
--- src/modules/frontend/newt/newt.c (revision 26694)
+++ src/modules/frontend/newt/newt.c (working copy)
@@ -41,6 +41,7 @@
#include "question.h"
#include "frontend.h"
#include "database.h"
+#include "plugin.h"
#include "strutl.h"
#include <ctype.h>
@@ -973,8 +974,11 @@
{ "error", newt_handler_error },
};
+/* includes plugin handlers */
+static struct question_handlers *all_question_handlers;
+
/*
- * Function: newt_intitialize
+ * Function: newt_initialize
* Input: struct frontend *obj - frontend UI object
* struct configuration *cfg - configuration parameters
* Output: int - DC_OK, DC_NOTOK
@@ -987,10 +991,25 @@
static int
newt_initialize(struct frontend *obj, struct configuration *conf)
{
- int i, width = 80, height = 24;
+ int i, questions, width = 80, height = 24;
obj->interactive = 1;
obj->data = calloc(1, sizeof(struct newt_data));
+
+ questions = sizeof(question_handlers) / sizeof(struct question_handlers);
+ all_question_handlers =
+ malloc((questions + obj->plugins->n_plugins + 1) *
+ sizeof(struct question_handlers));
+ memcpy(all_question_handlers, question_handlers, sizeof(question_handlers));
+ for (i = 0; i < obj->plugins->n_plugins; ++i) {
+ all_question_handlers[questions + i].type =
+ obj->plugins->plugins[i]->name;
+ all_question_handlers[questions + i].handler =
+ obj->plugins->plugins[i]->handler;
+ }
+ all_question_handlers[questions + i].type = NULL;
+ all_question_handlers[questions + i].handler = NULL;
+
newtInit();
newtGetScreenSize(&width, &height);
// Fill the screen so people can shift-pgup properly
@@ -1013,12 +1032,13 @@
{
struct newt_data *data = (struct newt_data *)obj->data;
struct question *q = obj->questions;
- int i, ret = DC_OK, cleared;
+ struct question_handlers *qhandler;
+ int ret = DC_OK, cleared;
cleared = 0;
while (q != NULL) {
- for (i = 0; i < DIM(question_handlers); i++) {
- if (strcmp(q->template->type, question_handlers[i].type) == 0) {
+ for (qhandler = all_question_handlers; qhandler->type; ++qhandler) {
+ if (strcmp(q->template->type, qhandler->type) == 0) {
if (!cleared && !data->scale_form) {
cleared = 1;
newtInit();
@@ -1030,7 +1050,7 @@
newtDrawRootText(0, 0, text);
free(text);
}
- ret = question_handlers[i].handler(obj, q);
+ ret = qhandler->handler(obj, q);
if (ret == DC_OK)
obj->qdb->methods.set(obj->qdb, q);
else if (ret == DC_GOBACK && q->prev != NULL)
Index: src/plugin.c
===================================================================
--- src/plugin.c (revision 0)
+++ src/plugin.c (revision 0)
@@ -0,0 +1,150 @@
+#include "common.h"
+#include "configuration.h"
+#include "plugin.h"
+
+#include <stdlib.h>
+#include <string.h>
+#include <sys/types.h>
+#include <dirent.h>
+#include <dlfcn.h>
+
+/* Convert hyphens to underscores. Returns allocated string. */
+static char *make_symbol_name(const char *name)
+{
+ char *symbol, *symbolp;
+
+ symbol = strdup(name);
+ for (symbolp = symbol; *symbolp; ++symbolp) {
+ if (*symbolp == '-')
+ *symbolp = '_';
+ }
+ return symbol;
+}
+
+struct plugin *plugin_new(struct plugin_db *db, const char *filename)
+{
+ struct plugin *plugin = NEW(struct plugin);
+ const char *base;
+ size_t baselen, symbollen;
+ char *typesymbol, *symbol;
+
+ base = strrchr(filename, '/');
+ if (base)
+ ++base;
+ else
+ base = filename;
+
+ baselen = strlen(base);
+ /* base must be plugin-<type>.so */
+ if (baselen < 11)
+ return NULL;
+ if (strncmp(base, "plugin-", 7) != 0)
+ return NULL;
+ if (strncmp(base + baselen - 3, ".so", 3) != 0)
+ return NULL;
+
+ plugin->name = malloc(baselen - 9);
+ strncpy(plugin->name, base + 7, baselen - 10);
+ plugin->name[baselen - 10] = '\0';
+ typesymbol = make_symbol_name(plugin->name);
+
+ plugin->module = dlopen(filename, RTLD_LAZY);
+ if (plugin->module == NULL) {
+ INFO(INFO_ERROR, "Cannot load plugin module %s: %s\n",
+ filename, dlerror());
+ free(plugin->name);
+ DELETE(plugin);
+ return NULL;
+ }
+
+ symbollen = strlen(db->frontend) + 9 + strlen(plugin->name) + 1;
+ symbol = malloc(symbollen);
+ snprintf(symbol, symbollen, "%s_handler_%s", db->frontend, typesymbol);
+
+ plugin->handler = dlsym(plugin->module, symbol);
+ if (plugin->handler == NULL) {
+ INFO(INFO_ERROR, "Malformed plugin module %s\n", filename);
+ dlclose(plugin->module);
+ free(plugin->name);
+ DELETE(plugin);
+ return NULL;
+ }
+
+ return plugin;
+}
+
+struct plugin_db *plugin_db_initialize(struct configuration *cfg,
+ const char *modname)
+{
+ struct plugin_db *db;
+ const char *modpath;
+ char tmp[256];
+ DIR *plugin_dir = NULL;
+ struct dirent *plugin_dirent;
+
+ modpath = cfg->get(cfg, "global::module_path::frontend", 0);
+ if (modpath == NULL)
+ /* warn? */
+ return NULL;
+
+ snprintf(tmp, sizeof(tmp), "%s/%s", modpath, modname);
+ plugin_dir = opendir(tmp);
+ if (!plugin_dir)
+ /* warn for != ENOENT? */
+ return NULL;
+
+ db = NEW(struct plugin_db);
+ db->frontend = strdup(modname);
+ db->plugins = NULL;
+ db->n_plugins = 0;
+ db->alloc_plugins = 0;
+
+ while ((plugin_dirent = readdir(plugin_dir)) != NULL) {
+ struct plugin *plugin;
+
+ snprintf(tmp, sizeof(tmp), "%s/%s/%s", modpath, modname,
+ plugin_dirent->d_name);
+ plugin = plugin_new(db, tmp);
+ if (plugin == NULL)
+ continue;
+
+ if (db->n_plugins >= db->alloc_plugins) {
+ if (db->alloc_plugins)
+ db->alloc_plugins *= 4;
+ else
+ db->alloc_plugins = 16;
+ db->plugins = realloc(db->plugins,
+ db->alloc_plugins * sizeof *db->plugins);
+ }
+ db->plugins[db->n_plugins++] = plugin;
+ }
+
+ closedir(plugin_dir);
+
+ return db;
+}
+
+char *plugin_db_capabilities(struct plugin_db *db)
+{
+ size_t capb_size = 0;
+ char *capb;
+ int i;
+
+ for (i = 0; i < db->n_plugins; ++i) {
+ if (i > 0)
+ ++capb_size; /* " " */
+ capb_size += 7 /* "plugin-" */ + strlen(db->plugins[i]->name);
+ }
+
+ capb = malloc(capb_size + 1);
+ capb[0] = '\0';
+
+ for (i = 0; i < db->n_plugins; ++i) {
+ if (i > 0)
+ strcat(capb, " ");
+ strcat(capb, "plugin-");
+ strcat(capb, db->plugins[i]->name);
+ }
+
+ return capb;
+}
Index: src/plugin.h
===================================================================
--- src/plugin.h (revision 0)
+++ src/plugin.h (revision 0)
@@ -0,0 +1,39 @@
+/**
+ * @file plugin.h
+ * @brief interfaces for handling custom debconf widget implementations
+ */
+#ifndef _PLUGIN_H_
+#define _PLUGIN_H_
+
+struct configuration;
+struct frontend;
+struct question;
+
+struct plugin {
+ /** type name */
+ char *name;
+ /** library module handle */
+ void *module;
+ /** handler function */
+ void *handler;
+};
+
+struct plugin_db {
+ /** frontend these plugins are for */
+ char *frontend;
+ /** all known plugins */
+ struct plugin **plugins;
+ /** number of plugins */
+ int n_plugins;
+ /** allocated space */
+ int alloc_plugins;
+};
+
+struct plugin *plugin_new(struct plugin_db *db, const char *filename);
+
+struct plugin_db *plugin_db_initialize(struct configuration *cfg,
+ const char *modname);
+
+char *plugin_db_capabilities(struct plugin_db *db);
+
+#endif
Reply to: