From 041b59e39d320fff89e02a853cd162ba88b909cf Mon Sep 17 00:00:00 2001 From: Max Kellermann Date: Fri, 12 Dec 2008 18:46:48 +0100 Subject: [PATCH] plugin: new plugin library The plugin library is based on code from lyrics.c. --- configure.ac | 2 + src/Makefile.am | 5 + src/plugin.c | 334 ++++++++++++++++++++++++++++++++++++++++++++++++ src/plugin.h | 92 +++++++++++++ 4 files changed, 433 insertions(+) create mode 100644 src/plugin.c create mode 100644 src/plugin.h diff --git a/configure.ac b/configure.ac index ebc19fd..1c14135 100644 --- a/configure.ac +++ b/configure.ac @@ -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], diff --git a/src/Makefile.am b/src/Makefile.am index c91df26..0078380 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -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 index 0000000..79515d8 --- /dev/null +++ b/src/plugin.c @@ -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 +#include +#include +#include +#include +#include +#include + +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 index 0000000..b8bf901 --- /dev/null +++ b/src/plugin.h @@ -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 +#include + +/** + * 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 -- 2.30.2