Code

plugin: new plugin library
authorMax Kellermann <max@duempel.org>
Fri, 12 Dec 2008 17:46:48 +0000 (18:46 +0100)
committerMax Kellermann <max@duempel.org>
Fri, 12 Dec 2008 17:46:48 +0000 (18:46 +0100)
The plugin library is based on code from lyrics.c.

configure.ac
src/Makefile.am
src/plugin.c [new file with mode: 0644]
src/plugin.h [new file with mode: 0644]

index ebc19fdeaddec6467b9d795fc4a1d2fca16fd176..1c141358746b19b135b503ca429457f2423f9799 100644 (file)
@@ -366,6 +366,8 @@ AC_DEFINE_UNQUOTED([LYRICS_PLUGIN_DIR], ["$lyrics_plugin_dir"],
                   [Directory to search for lyrics plugins])
 AC_SUBST(lyrics_plugin_dir)
 
+AM_CONDITIONAL(ENABLE_PLUGIN_LIBRARY, test x$lyrics_screen = xyes)
+
 dnl Optional screen - outputs
 AC_MSG_CHECKING([whether to include the outputs screen])
 AC_ARG_ENABLE([outputs-screen],
index c91df2676a5d82f89c2531832ddc502361c89234..00783807fd886e84cd2cc1b93896f1786fef3256 100644 (file)
@@ -39,6 +39,7 @@ ncmpc_headers = \
        defaults.h \
        i18n.h \
        screen_browser.h \
+       plugin.h \
        lyrics.h \
        str_pool.h \
        lirc.h
@@ -100,6 +101,10 @@ if ENABLE_KEYDEF_SCREEN
 ncmpc_SOURCES += screen_keydef.c
 endif
 
+if ENABLE_PLUGIN_LIBRARY
+ncmpc_SOURCES += plugin.c
+endif
+
 if ENABLE_LYRICS_SCREEN
 ncmpc_SOURCES += screen_lyrics.c lyrics.c
 endif
diff --git a/src/plugin.c b/src/plugin.c
new file mode 100644 (file)
index 0000000..79515d8
--- /dev/null
@@ -0,0 +1,334 @@
+/*
+ * (c) 2004-2008 The Music Player Daemon Project
+ * http://www.musicpd.org/
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+#include "plugin.h"
+
+#include <assert.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/signal.h>
+#include <sys/wait.h>
+
+struct plugin_cycle {
+       /** the plugin list; used for traversing to the next plugin */
+       struct plugin_list *list;
+
+       /** arguments passed to execv() */
+       char **argv;
+
+       /** caller defined callback function */
+       plugin_callback_t callback;
+       /** caller defined pointer passed to #callback */
+       void *callback_data;
+
+       /** the index of the next plugin which is going to be
+           invoked */
+       guint next_plugin;
+
+       /** the pid of the plugin process, or -1 if none is currently
+           running */
+       pid_t pid;
+       /** the pipe to the plugin process, or -1 if none is currently
+           open */
+       int fd;
+       /** the GLib channel of #fd */
+       GIOChannel *channel;
+       /** the GLib IO watch of #channel */
+       guint event_id;
+
+       /** the output of the current plugin */
+       GString *data;
+};
+
+static bool
+register_plugin(struct plugin_list *list, char *path)
+{
+       int ret;
+       struct stat st;
+
+       ret = stat(path, &st);
+       if (ret < 0)
+               return false;
+
+       g_ptr_array_add(list->plugins, path);
+       return true;
+}
+
+bool
+plugin_list_load_directory(struct plugin_list *list, const char *path)
+{
+       GDir *dir;
+       const char *name;
+       char *plugin;
+       bool ret;
+
+       dir = g_dir_open(path, 0, NULL);
+       if (dir == NULL)
+               return false;
+
+       while ((name = g_dir_read_name(dir)) != NULL) {
+               plugin = g_build_filename(path, name, NULL);
+               ret = register_plugin(list, plugin);
+               if (!ret)
+                       g_free(plugin);
+       }
+
+       g_dir_close(dir);
+       return true;
+}
+
+void plugin_list_deinit(struct plugin_list *list)
+{
+       for (guint i = 0; i < list->plugins->len; ++i)
+               free(g_ptr_array_index(list->plugins, i));
+       g_ptr_array_free(list->plugins, TRUE);
+}
+
+static void
+next_plugin(struct plugin_cycle *cycle);
+
+static void
+plugin_eof(struct plugin_cycle *cycle)
+{
+       int ret, status;
+
+       g_io_channel_unref(cycle->channel);
+       close(cycle->fd);
+       cycle->fd = -1;
+
+       ret = waitpid(cycle->pid, &status, 0);
+       cycle->pid = -1;
+
+       if (ret < 0 || !WIFEXITED(status) || WEXITSTATUS(status) != 0) {
+               /* the plugin has failed */
+               g_string_free(cycle->data, TRUE);
+               cycle->data = NULL;
+
+               next_plugin(cycle);
+       } else {
+               /* success: invoke the callback */
+               cycle->callback(cycle->data, cycle->callback_data);
+       }
+}
+
+static gboolean
+plugin_data(G_GNUC_UNUSED GIOChannel *source,
+           G_GNUC_UNUSED GIOCondition condition, gpointer data)
+{
+       struct plugin_cycle *cycle = data;
+       char buffer[256];
+       ssize_t nbytes;
+
+       assert(cycle != NULL);
+       assert(cycle->fd >= 0);
+       assert(cycle->pid > 0);
+       assert(source == cycle->channel);
+
+       nbytes = condition & G_IO_IN
+               ? read(cycle->fd, buffer, sizeof(buffer))
+               : 0;
+       if (nbytes <= 0) {
+               plugin_eof(cycle);
+               return FALSE;
+       }
+
+       g_string_append_len(cycle->data, buffer, nbytes);
+       return TRUE;
+}
+
+/**
+ * This is a timer callback which calls the plugin callback "some time
+ * later".  This solves the problem that plugin_run() may fail
+ * immediately, leaving its return value in an undefined state.
+ * Instead, install a timer which calls the plugin callback in the
+ * moment after.
+ */
+static gboolean
+plugin_delayed_fail(gpointer data)
+{
+       struct plugin_cycle *cycle = data;
+
+       assert(cycle != NULL);
+       assert(cycle->fd < 0);
+       assert(cycle->pid < 0);
+       assert(cycle->data == NULL);
+
+       cycle->callback(NULL, cycle->callback_data);
+
+       return FALSE;
+}
+
+static int
+start_plugin(struct plugin_cycle *cycle, const char *plugin_path)
+{
+       int ret, fds[2];
+       pid_t pid;
+
+       assert(cycle != NULL);
+       assert(cycle->pid < 0);
+       assert(cycle->fd < 0);
+       assert(cycle->data == NULL);
+
+       /* set new program name, but free the one from the previous
+          plugin */
+       g_free(cycle->argv[0]);
+       cycle->argv[0] = g_path_get_basename(plugin_path);
+
+       ret = pipe(fds);
+       if (ret < 0)
+               return -1;
+
+       pid = fork();
+
+       if (pid < 0) {
+               close(fds[0]);
+               close(fds[1]);
+               return -1;
+       }
+
+       if (pid == 0) {
+               dup2(fds[1], 1);
+               dup2(fds[1], 1);
+               close(fds[0]);
+               close(fds[1]);
+               close(0);
+               /* XXX close other fds? */
+
+               execv(plugin_path, cycle->argv);
+               _exit(1);
+       }
+
+       close(fds[1]);
+
+       cycle->pid = pid;
+       cycle->fd = fds[0];
+       cycle->data = g_string_new(NULL);
+
+       /* XXX CLOEXEC? */
+
+       cycle->channel = g_io_channel_unix_new(cycle->fd);
+       cycle->event_id = g_io_add_watch(cycle->channel, G_IO_IN|G_IO_HUP,
+                                        plugin_data, cycle);
+
+       return 0;
+}
+
+static void
+next_plugin(struct plugin_cycle *cycle)
+{
+       const char *plugin_path;
+       int ret = -1;
+
+       assert(cycle->pid < 0);
+       assert(cycle->fd < 0);
+       assert(cycle->data == NULL);
+
+       if (cycle->next_plugin >= cycle->list->plugins->len) {
+               /* no plugins left */
+               g_timeout_add(0, plugin_delayed_fail, cycle);
+               return;
+       }
+
+       plugin_path = g_ptr_array_index(cycle->list->plugins,
+                                       cycle->next_plugin++);
+       ret = start_plugin(cycle, plugin_path);
+       if (ret < 0) {
+               /* system error */
+               g_timeout_add(0, plugin_delayed_fail, cycle);
+               return;
+       }
+}
+
+static char **
+make_argv(const char*const* args)
+{
+       unsigned num = 0;
+       char **ret;
+
+       while (args[num] != NULL)
+               ++num;
+       num += 2;
+
+       ret = g_new(char*, num);
+
+       /* reserve space for the program name */
+       *ret++ = NULL;
+
+       do {
+               *ret++ = g_strdup(*args++);
+       } while (*args != NULL);
+
+       /* end of argument vector */
+       *ret++ = NULL;
+
+       return ret - num;
+}
+
+struct plugin_cycle *
+plugin_run(struct plugin_list *list, const char *const*args,
+          plugin_callback_t callback, void *callback_data)
+{
+       struct plugin_cycle *cycle = g_new(struct plugin_cycle, 1);
+
+       assert(args != NULL);
+
+       cycle->list = list;
+       cycle->argv = make_argv(args);
+       cycle->callback = callback;
+       cycle->callback_data = callback_data;
+       cycle->next_plugin = 0;
+       cycle->pid = -1;
+       cycle->fd = -1;
+       cycle->data = NULL;
+
+       next_plugin(cycle);
+
+       return cycle;
+}
+
+void
+plugin_stop(struct plugin_cycle *cycle)
+{
+       if (cycle->fd >= 0) {
+               /* close the pipe to the plugin */
+               g_source_remove(cycle->event_id);
+               g_io_channel_unref(cycle->channel);
+               close(cycle->fd);
+       }
+
+       if (cycle->pid > 0) {
+               /* kill the plugin process */
+               int status;
+
+               kill(cycle->pid, SIGTERM);
+               waitpid(cycle->pid, &status, 0);
+       }
+
+       if (cycle->data != NULL)
+               /* free data that has been received */
+               g_string_free(cycle->data, TRUE);
+
+       /* free argument list */
+       for (guint i = 0; i == 0 || cycle->argv[i] != NULL; ++i)
+               g_free(cycle->argv[i]);
+       g_free(cycle->argv);
+
+       g_free(cycle);
+}
diff --git a/src/plugin.h b/src/plugin.h
new file mode 100644 (file)
index 0000000..b8bf901
--- /dev/null
@@ -0,0 +1,92 @@
+/*
+ * (c) 2004-2008 The Music Player Daemon Project
+ * http://www.musicpd.org/
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+#ifndef PLUGIN_H
+#define PLUGIN_H
+
+#include <glib.h>
+#include <stdbool.h>
+
+/**
+ * A list of registered plugins.
+ */
+struct plugin_list {
+       GPtrArray *plugins;
+};
+
+/**
+ * When a plugin cycle is finished, this function is called.  In any
+ * case, plugin_stop() has to be called to free all memory.
+ *
+ * @param result the plugin's output (stdout) on success; NULL on failure
+ * @param data the caller defined pointer passed to plugin_run()
+ */
+typedef void (*plugin_callback_t)(const GString *result, void *data);
+
+/**
+ * This object represents a cycle through all available plugins, until
+ * a plugin returns a positive result.
+ */
+struct plugin_cycle;
+
+/**
+ * Initialize an empty plugin_list structure.
+ */
+static inline void
+plugin_list_init(struct plugin_list *list)
+{
+       list->plugins = g_ptr_array_new();
+}
+
+/**
+ * Load all plugins (executables) in a directory.
+ */
+bool
+plugin_list_load_directory(struct plugin_list *list, const char *path);
+
+/**
+ * Frees all memory held by the plugin_list object (but not the
+ * pointer itself).
+ */
+void plugin_list_deinit(struct plugin_list *list);
+
+/**
+ * Run plugins in this list, until one returns success (or until the
+ * plugin list is exhausted).
+ *
+ * @param list the plugin list
+ * @param args NULL terminated command line arguments passed to the
+ * plugin programs
+ * @param callback the callback function which will be called when a
+ * result is available
+ * @param callback_data caller defined pointer which is passed to the
+ * callback function
+ */
+struct plugin_cycle *
+plugin_run(struct plugin_list *list, const char *const*args,
+          plugin_callback_t callback, void *callback_data);
+
+/**
+ * Stops the plugin cycle and frees resources.  This can be called to
+ * abort the current cycle, or after the plugin_callback_t has been
+ * invoked.
+ */
+void
+plugin_stop(struct plugin_cycle *invocation);
+
+#endif