diff options
author | Max Kellermann <max@duempel.org> | 2009-09-25 18:32:00 +0200 |
---|---|---|
committer | Max Kellermann <max@duempel.org> | 2009-09-25 18:32:00 +0200 |
commit | 8f261af5c1cfd6f922df009e17756fa3bef71458 (patch) | |
tree | dd5dd4bd6d41d1536e31f32e502e473e40520663 /src | |
parent | 3e8bdb93844eb21f4a34d4f586e51f869933ebb8 (diff) | |
download | mpd-8f261af5c1cfd6f922df009e17756fa3bef71458.tar.gz mpd-8f261af5c1cfd6f922df009e17756fa3bef71458.tar.xz mpd-8f261af5c1cfd6f922df009e17756fa3bef71458.zip |
automatically update the database with Linux inotify
This patch implements a light-weight inotify library, and watches all
directories below the music directory. It updates all directories
where files changed after a delay of 5 seconds.
Diffstat (limited to '')
-rw-r--r-- | src/inotify_queue.c | 134 | ||||
-rw-r--r-- | src/inotify_queue.h | 32 | ||||
-rw-r--r-- | src/inotify_source.c | 163 | ||||
-rw-r--r-- | src/inotify_source.h | 61 | ||||
-rw-r--r-- | src/inotify_update.c | 349 | ||||
-rw-r--r-- | src/inotify_update.h | 47 | ||||
-rw-r--r-- | src/main.c | 6 |
7 files changed, 792 insertions, 0 deletions
diff --git a/src/inotify_queue.c b/src/inotify_queue.c new file mode 100644 index 000000000..da5a215f8 --- /dev/null +++ b/src/inotify_queue.c @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2003-2009 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., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "inotify_queue.h" +#include "update.h" + +#include <glib.h> + +#include <string.h> + +#undef G_LOG_DOMAIN +#define G_LOG_DOMAIN "inotify" + +enum { + /** + * Wait this long after the last change before calling + * update_enqueue(). This increases the probability that + * updates can be bundled. + */ + INOTIFY_UPDATE_DELAY_MS = 5000, +}; + +static GSList *inotify_queue; +static guint queue_source_id; + +void +mpd_inotify_queue_init(void) +{ +} + +static void +free_callback(gpointer data, G_GNUC_UNUSED gpointer user_data) +{ + g_free(data); +} + +void +mpd_inotify_queue_finish(void) +{ + if (queue_source_id != 0) + g_source_remove(queue_source_id); + + g_slist_foreach(inotify_queue, free_callback, NULL); + g_slist_free(inotify_queue); +} + +static gboolean +mpd_inotify_run_update(G_GNUC_UNUSED gpointer data) +{ + unsigned id; + + while (inotify_queue != NULL) { + char *uri_utf8 = inotify_queue->data; + + id = update_enqueue(uri_utf8, false); + if (id == 0) + /* retry later */ + return true; + + g_debug("updating '%s' job=%u", uri_utf8, id); + + g_free(uri_utf8); + inotify_queue = g_slist_delete_link(inotify_queue, + inotify_queue); + } + + /* done, remove the timer event by returning false */ + queue_source_id = 0; + return false; +} + +static bool +path_in(const char *path, const char *possible_parent) +{ + size_t length = strlen(possible_parent); + + return path[0] == 0 || + (memcmp(possible_parent, path, length) == 0 && + (path[length] == 0 || path[length] == '/')); +} + +void +mpd_inotify_enqueue(char *uri_utf8) +{ + GSList *old_queue = inotify_queue; + + if (queue_source_id != 0) + g_source_remove(queue_source_id); + queue_source_id = g_timeout_add(INOTIFY_UPDATE_DELAY_MS, + mpd_inotify_run_update, NULL); + + inotify_queue = NULL; + while (old_queue != NULL) { + char *current_uri = old_queue->data; + + if (path_in(uri_utf8, current_uri)) { + /* already enqueued */ + g_free(uri_utf8); + inotify_queue = g_slist_concat(inotify_queue, + old_queue); + return; + } + + old_queue = g_slist_delete_link(old_queue, old_queue); + + if (path_in(current_uri, uri_utf8)) + /* existing path is a sub-path of the new + path; we can dequeue the existing path and + update the new path instead */ + g_free(current_uri); + else + /* move the existing path to the new queue */ + inotify_queue = g_slist_prepend(inotify_queue, + current_uri); + } + + inotify_queue = g_slist_prepend(inotify_queue, uri_utf8); +} diff --git a/src/inotify_queue.h b/src/inotify_queue.h new file mode 100644 index 000000000..6e12e9bda --- /dev/null +++ b/src/inotify_queue.h @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2003-2009 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., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_INOTIFY_QUEUE_H +#define MPD_INOTIFY_QUEUE_H + +void +mpd_inotify_queue_init(void); + +void +mpd_inotify_queue_finish(void); + +void +mpd_inotify_enqueue(char *uri_utf8); + +#endif diff --git a/src/inotify_source.c b/src/inotify_source.c new file mode 100644 index 000000000..d5feec3e0 --- /dev/null +++ b/src/inotify_source.c @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2003-2009 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., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "inotify_source.h" +#include "fifo_buffer.h" + +#include <sys/inotify.h> +#include <unistd.h> +#include <errno.h> +#include <stdbool.h> + +#undef G_LOG_DOMAIN +#define G_LOG_DOMAIN "inotify" + +struct mpd_inotify_source { + int fd; + + GIOChannel *channel; + + /** + * The channel's source id in the GLib main loop. + */ + guint id; + + struct fifo_buffer *buffer; + + mpd_inotify_callback_t callback; + void *callback_ctx; +}; + +/** + * A GQuark for GError instances. + */ +static inline GQuark +mpd_inotify_quark(void) +{ + return g_quark_from_static_string("inotify"); +} + +static gboolean +mpd_inotify_in_event(G_GNUC_UNUSED GIOChannel *_source, + G_GNUC_UNUSED GIOCondition condition, + gpointer data) +{ + struct mpd_inotify_source *source = data; + void *dest; + size_t length; + ssize_t nbytes; + const struct inotify_event *event; + + dest = fifo_buffer_write(source->buffer, &length); + if (dest == NULL) + g_error("buffer full"); + + nbytes = read(source->fd, dest, length); + if (nbytes < 0) + g_error("failed to read from inotify: %s", g_strerror(errno)); + if (nbytes == 0) + g_error("end of file from inotify"); + + fifo_buffer_append(source->buffer, nbytes); + + while (true) { + const char *name; + + event = fifo_buffer_read(source->buffer, &length); + if (event == NULL || length < sizeof(*event) || + length < sizeof(*event) + event->len) + break; + + if (event->len > 0 && event->name[event->len - 1] == 0) + name = event->name; + else + name = NULL; + + source->callback(event->wd, event->mask, name, + source->callback_ctx); + fifo_buffer_consume(source->buffer, + sizeof(*event) + event->len); + } + + return true; +} + +struct mpd_inotify_source * +mpd_inotify_source_new(mpd_inotify_callback_t callback, void *callback_ctx, + GError **error_r) +{ + struct mpd_inotify_source *source = + g_new(struct mpd_inotify_source, 1); + + source->fd = inotify_init(); + if (source->fd < 0) { + g_set_error(error_r, mpd_inotify_quark(), errno, + "inotify_init() has failed: %s", + g_strerror(errno)); + g_free(source); + return NULL; + } + + source->buffer = fifo_buffer_new(4096); + + source->channel = g_io_channel_unix_new(source->fd); + source->id = g_io_add_watch(source->channel, G_IO_IN, + mpd_inotify_in_event, source); + + source->callback = callback; + source->callback_ctx = callback_ctx; + + return source; +} + +void +mpd_inotify_source_free(struct mpd_inotify_source *source) +{ + g_source_remove(source->id); + g_io_channel_unref(source->channel); + fifo_buffer_free(source->buffer); + close(source->fd); + g_free(source); +} + +int +mpd_inotify_source_add(struct mpd_inotify_source *source, + const char *path_fs, unsigned mask, + GError **error_r) +{ + int wd = inotify_add_watch(source->fd, path_fs, mask); + if (wd < 0) + g_set_error(error_r, mpd_inotify_quark(), errno, + "inotify_add_watch() has failed: %s", + g_strerror(errno)); + + return wd; +} + +void +mpd_inotify_source_rm(struct mpd_inotify_source *source, unsigned wd) +{ + int ret = inotify_rm_watch(source->fd, wd); + if (ret < 0 && errno != EINVAL) + g_warning("inotify_rm_watch() has failed: %s", + g_strerror(errno)); + + /* EINVAL may happen here when the file has been deleted; the + kernel seems to auto-unregister deleted files */ +} diff --git a/src/inotify_source.h b/src/inotify_source.h new file mode 100644 index 000000000..eb609ccfc --- /dev/null +++ b/src/inotify_source.h @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2003-2009 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., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_INOTIFY_SOURCE_H +#define MPD_INOTIFY_SOURCE_H + +#include <glib.h> + +typedef void (*mpd_inotify_callback_t)(int wd, unsigned mask, + const char *name, void *ctx); + +struct mpd_inotify_source; + +/** + * Creates a new inotify source and registers it in the GLib main + * loop. + * + * @param a callback invoked for events received from the kernel + */ +struct mpd_inotify_source * +mpd_inotify_source_new(mpd_inotify_callback_t callback, void *callback_ctx, + GError **error_r); + +void +mpd_inotify_source_free(struct mpd_inotify_source *source); + +/** + * Adds a path to the notify list. + * + * @return a watch descriptor or -1 on error + */ +int +mpd_inotify_source_add(struct mpd_inotify_source *source, + const char *path_fs, unsigned mask, + GError **error_r); + +/** + * Removes a path from the notify list. + * + * @param wd the watch descriptor returned by mpd_inotify_source_add() + */ +void +mpd_inotify_source_rm(struct mpd_inotify_source *source, unsigned wd); + +#endif diff --git a/src/inotify_update.c b/src/inotify_update.c new file mode 100644 index 000000000..996afb0aa --- /dev/null +++ b/src/inotify_update.c @@ -0,0 +1,349 @@ +/* + * Copyright (C) 2003-2009 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., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "inotify_update.h" +#include "inotify_source.h" +#include "inotify_queue.h" +#include "database.h" +#include "mapper.h" +#include "path.h" + +#include <assert.h> +#include <sys/inotify.h> +#include <sys/stat.h> +#include <stdbool.h> +#include <string.h> +#include <dirent.h> +#include <errno.h> + +#undef G_LOG_DOMAIN +#define G_LOG_DOMAIN "inotify" + +enum { + IN_MASK = IN_ATTRIB|IN_CLOSE_WRITE|IN_CREATE|IN_DELETE|IN_DELETE_SELF + |IN_MOVE|IN_MOVE_SELF +#ifdef IN_ONLYDIR + |IN_ONLYDIR +#endif +}; + +struct watch_directory { + struct watch_directory *parent; + + char *name; + + int descriptor; + + GList *children; +}; + +static struct mpd_inotify_source *inotify_source; + +static struct watch_directory inotify_root; +static GTree *inotify_directories; + +static gint +compare(gconstpointer a, gconstpointer b) +{ + if (a < b) + return -1; + else if (a > b) + return 1; + else + return 0; +} + +static void +tree_add_watch_directory(struct watch_directory *directory) +{ + g_tree_insert(inotify_directories, + GINT_TO_POINTER(directory->descriptor), directory); +} + +static void +tree_remove_watch_directory(struct watch_directory *directory) +{ + bool found = g_tree_remove(inotify_directories, + GINT_TO_POINTER(directory->descriptor)); + assert(found); +} + +static struct watch_directory * +tree_find_watch_directory(int wd) +{ + return g_tree_lookup(inotify_directories, GINT_TO_POINTER(wd)); +} + +static void +remove_watch_directory(struct watch_directory *directory) +{ + assert(directory != NULL); + assert(directory->parent != NULL); + assert(directory->parent->children != NULL); + + tree_remove_watch_directory(directory); + + while (directory->children != NULL) + remove_watch_directory(directory->children->data); + + directory->parent->children = + g_list_remove(directory->parent->children, directory); + + mpd_inotify_source_rm(inotify_source, directory->descriptor); + g_free(directory->name); + g_slice_free(struct watch_directory, directory); +} + +static char * +watch_directory_get_uri_fs(const struct watch_directory *directory) +{ + char *parent_uri, *uri; + + if (directory->parent == NULL) + return NULL; + + parent_uri = watch_directory_get_uri_fs(directory->parent); + if (parent_uri == NULL) + return g_strdup(directory->name); + + uri = g_strconcat(parent_uri, "/", directory->name, NULL); + g_free(parent_uri); + + return uri; +} + +/* we don't look at "." / ".." nor files with newlines in their name */ +static bool skip_path(const char *path) +{ + return (path[0] == '.' && path[1] == 0) || + (path[0] == '.' && path[1] == '.' && path[2] == 0) || + strchr(path, '\n') != NULL; +} + +static void +recursive_watch_subdirectories(struct watch_directory *directory, + const char *path_fs) +{ + GError *error = NULL; + DIR *dir; + struct dirent *ent; + + assert(directory != NULL); + assert(path_fs != NULL); + + dir = opendir(path_fs); + if (dir == NULL) { + g_warning("Failed to open directory %s: %s", + path_fs, g_strerror(errno)); + return; + } + + while ((ent = readdir(dir))) { + char *child_path_fs; + struct stat st; + int ret; + struct watch_directory *child; + + if (skip_path(ent->d_name)) + continue; + + child_path_fs = g_strconcat(path_fs, "/", ent->d_name, NULL); + /* XXX what about symlinks? */ + ret = lstat(child_path_fs, &st); + if (ret < 0) { + g_warning("Failed to stat %s: %s", + child_path_fs, g_strerror(errno)); + g_free(child_path_fs); + continue; + } + + if (!S_ISDIR(st.st_mode)) { + g_free(child_path_fs); + continue; + } + + ret = mpd_inotify_source_add(inotify_source, child_path_fs, + IN_MASK, &error); + if (ret < 0) { + g_warning("Failed to register %s: %s", + child_path_fs, error->message); + g_error_free(error); + error = NULL; + g_free(child_path_fs); + continue; + } + + child = tree_find_watch_directory(ret); + if (child != NULL) { + /* already being watched */ + g_free(child_path_fs); + continue; + } + + child = g_slice_new(struct watch_directory); + child->parent = directory; + child->name = g_strdup(ent->d_name); + child->descriptor = ret; + child->children = NULL; + + directory->children = g_list_prepend(directory->children, + child); + + tree_add_watch_directory(child); + + recursive_watch_subdirectories(child, child_path_fs); + g_free(child_path_fs); + } + + closedir(dir); +} + +static void +mpd_inotify_callback(int wd, unsigned mask, + G_GNUC_UNUSED const char *name, G_GNUC_UNUSED void *ctx) +{ + struct watch_directory *directory; + char *uri_fs; + + /*g_debug("wd=%d mask=0x%x name='%s'", wd, mask, name);*/ + + directory = tree_find_watch_directory(wd); + if (directory == NULL) + return; + + uri_fs = watch_directory_get_uri_fs(directory); + + if ((mask & (IN_DELETE_SELF|IN_MOVE_SELF)) != 0) { + g_free(uri_fs); + remove_watch_directory(directory); + return; + } + + if ((mask & (IN_ATTRIB|IN_CREATE|IN_MOVE)) != 0 && + (mask & IN_ISDIR) != 0) { + /* a sub directory was changed: register those in + inotify */ + char *root = map_directory_fs(db_get_root()); + char *path_fs; + + if (uri_fs != NULL) { + path_fs = g_strconcat(root, "/", uri_fs, NULL); + g_free(root); + } else + path_fs = root; + + recursive_watch_subdirectories(directory, path_fs); + g_free(path_fs); + } + + if ((mask & (IN_CLOSE_WRITE|IN_MOVE|IN_DELETE)) != 0) { + /* a file was changed, or a direectory was + moved/deleted: queue a database update */ + char *uri_utf8 = uri_fs != NULL + ? fs_charset_to_utf8(uri_fs) + : g_strdup(""); + + if (uri_utf8 != NULL) + /* this function will take care of freeing + uri_utf8 */ + mpd_inotify_enqueue(uri_utf8); + } + + g_free(uri_fs); +} + +void +mpd_inotify_init(void) +{ + struct directory *root; + char *path; + GError *error = NULL; + + g_debug("initializing inotify"); + + root = db_get_root(); + if (root == NULL) { + g_debug("no music directory configured"); + return; + } + + path = map_directory_fs(root); + if (path == NULL) { + g_warning("mapper has failed"); + return; + } + + inotify_source = mpd_inotify_source_new(mpd_inotify_callback, NULL, + &error); + if (inotify_source == NULL) { + g_warning("%s", error->message); + g_error_free(error); + g_free(path); + return; + } + + inotify_root.name = path; + inotify_root.descriptor = mpd_inotify_source_add(inotify_source, path, + IN_MASK, &error); + if (inotify_root.descriptor < 0) { + g_warning("%s", error->message); + g_error_free(error); + mpd_inotify_source_free(inotify_source); + inotify_source = NULL; + g_free(path); + return; + } + + inotify_directories = g_tree_new(compare); + tree_add_watch_directory(&inotify_root); + + recursive_watch_subdirectories(&inotify_root, path); + + mpd_inotify_queue_init(); + + g_debug("watching music directory"); +} + +static gboolean +free_watch_directory(G_GNUC_UNUSED gpointer key, gpointer value, + G_GNUC_UNUSED gpointer data) +{ + struct watch_directory *directory = value; + + g_free(directory->name); + g_list_free(directory->children); + + if (directory != &inotify_root) + g_slice_free(struct watch_directory, directory); + + return false; +} + +void +mpd_inotify_finish(void) +{ + if (inotify_source == NULL) + return; + + mpd_inotify_queue_finish(); + mpd_inotify_source_free(inotify_source); + + g_tree_foreach(inotify_directories, free_watch_directory, NULL); + g_tree_destroy(inotify_directories); +} diff --git a/src/inotify_update.h b/src/inotify_update.h new file mode 100644 index 000000000..45466afae --- /dev/null +++ b/src/inotify_update.h @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2003-2009 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., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_INOTIFY_UPDATE_H +#define MPD_INOTIFY_UPDATE_H + +#include "config.h" + +#ifdef HAVE_INOTIFY_INIT + +void +mpd_inotify_init(void); + +void +mpd_inotify_finish(void); + +#else /* !HAVE_INOTIFY_INIT */ + +static inline void +mpd_inotify_init(void) +{ +} + +static inline void +mpd_inotify_finish(void) +{ +} + +#endif /* !HAVE_INOTIFY_INIT */ + +#endif diff --git a/src/main.c b/src/main.c index 56627b665..bb0d09eaf 100644 --- a/src/main.c +++ b/src/main.c @@ -55,6 +55,7 @@ #include "dirvec.h" #include "songvec.h" #include "tag_pool.h" +#include "inotify_update.h" #ifdef ENABLE_SQLITE #include "sticker.h" @@ -369,6 +370,9 @@ int main(int argc, char *argv[]) glue_state_file_init(); + if (mapper_has_music_directory()) + mpd_inotify_init(); + config_global_check(); /* run the main loop */ @@ -379,6 +383,8 @@ int main(int argc, char *argv[]) g_main_loop_unref(main_loop); + mpd_inotify_finish(); + state_file_finish(); playerKill(); finishZeroconf(); |