[Date Prev][Date Next] [Thread Prev][Thread Next] [Date Index] [Thread Index]

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: