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.
This commit is contained in:
Max Kellermann 2009-09-25 18:32:00 +02:00
parent 3e8bdb9384
commit 8f261af5c1
10 changed files with 806 additions and 0 deletions

View File

@ -74,6 +74,9 @@ mpd_headers = \
src/fifo_buffer.h \
src/update.h \
src/update_internal.h \
src/inotify_source.h \
src/inotify_queue.h \
src/inotify_update.h \
src/dirvec.h \
src/gcc.h \
src/decoder_list.h \
@ -275,6 +278,13 @@ src_mpd_SOURCES = \
src/stored_playlist.c \
src/timer.c
if HAVE_INOTIFY
src_mpd_SOURCES += \
src/inotify_source.c \
src/inotify_queue.c \
src/inotify_update.c
endif
if ENABLE_SQLITE
src_mpd_SOURCES += \
src/sticker.c \

1
NEWS
View File

@ -34,6 +34,7 @@ ver 0.16 (20??/??/??)
* save state when stopped
* renamed option "--stdout" to "--stderr"
* removed options --create-db and --no-create-db
* automatically update the database with Linux inotify
* obey $(sysconfdir) for default mpd.conf location

View File

@ -118,6 +118,9 @@ AC_CHECK_LIB(m,exp,MPD_LIBS="$MPD_LIBS -lm",)
AC_CHECK_HEADERS(locale.h)
AC_CHECK_HEADERS(valgrind/memcheck.h)
AC_CHECK_FUNCS(inotify_init)
AM_CONDITIONAL(HAVE_INOTIFY, test x$ac_cv_func_inotify_init = xyes)
dnl
dnl mandatory libraries

134
src/inotify_queue.c Normal file
View File

@ -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);
}

32
src/inotify_queue.h Normal file
View File

@ -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

163
src/inotify_source.c Normal file
View File

@ -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 */
}

61
src/inotify_source.h Normal file
View File

@ -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

349
src/inotify_update.c Normal file
View File

@ -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);
}

47
src/inotify_update.h Normal file
View File

@ -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

View File

@ -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();