From: Dana Jansens Date: Mon, 20 Sep 2010 19:45:36 +0000 (-0400) Subject: Add/fix inotify support for watching filesystem changes. X-Git-Url: http://git.openbox.org/?a=commitdiff_plain;h=9437d183492745b87b8c0b67b5a6dddf4508c0a1;p=dana%2Fopenbox.git Add/fix inotify support for watching filesystem changes. obt/watch.h has the interface. obt/watch.c has the generic watch code. obt/watch_inotify.c has an inotify-specific backend, which could be replaced with another. --- diff --git a/Makefile.am b/Makefile.am index c4c9057e..426fe1ee 100644 --- a/Makefile.am +++ b/Makefile.am @@ -157,6 +157,7 @@ obt_libobt_la_SOURCES = \ obt/util.h \ obt/watch.h \ obt/watch.c \ + obt/watch_inotify.c \ obt/xqueue.h \ obt/xqueue.c diff --git a/obt/watch.c b/obt/watch.c index 3ed2d954..63a2ccef 100644 --- a/obt/watch.c +++ b/obt/watch.c @@ -18,63 +18,79 @@ #include "obt/watch.h" -#ifdef HAVE_SYS_INOTIFY_H -# include -#endif #ifdef HAVE_UNISTD_H # include #endif -#include + +#include + +typedef struct _ObtWatchTarget ObtWatchTarget; + +/*! Callback function for the system-specific GSource to alert us to changes. +*/ +typedef void (*ObtWatchNotifyFunc)(const gchar *path, gpointer target, + ObtWatchNotifyType type); + + +/* Interface for system-specific stuff (e.g. inotify). the functions are + defined in in watch_.c +*/ + +/*! Initializes the watch subsystem, and returns a GSource for it. + @param notify The GSource will call @notify when a watched file is changed. + @return Returns a GSource* on success, and a NULL if an error occurred. +*/ +GSource* watch_sys_create_source(ObtWatchNotifyFunc notify); +/*! Add a target to the watch subsystem. + @return Returns an integer key that is used to uniquely identify the target + within this subsystem. A negative value indicates an error. +*/ +gint watch_sys_add_target(GSource *source, const char *path, + gboolean watch_hidden, gpointer target); +/*! Remove a target from the watch system, by its key. + Use the key returned from watch_sys_add_target() to remove the target. +*/ +void watch_sys_remove_target(GSource *source, gint key); + + +/* General system which uses the watch_sys_* stuff +*/ struct _ObtWatch { guint ref; - gint ino_fd; - guint ino_watch; - GHashTable *targets; - -#ifdef HAVE_SYS_INOTIFY_H - GHashTable *targets_by_wd; -#endif + GHashTable *targets_by_path; + GSource *source; }; -typedef struct _ObtWatchTarget { +struct _ObtWatchTarget { ObtWatch *w; - -#ifdef HAVE_SYS_INOTIFY_H - gint wd; -#endif - - gchar *path; + gchar *base_path; ObtWatchFunc func; gpointer data; -} ObtWatchTarget; - -static void init_inot(ObtWatch *w); -static gboolean read_inot(GIOChannel *s, GIOCondition cond, gpointer data); -static gboolean add_inot(ObtWatch *w, ObtWatchTarget *t, const char *path, - gboolean dir); -static void rm_inot(ObtWatchTarget *t); -static ObtWatchTarget* target_new(ObtWatch *w, const gchar *path, - ObtWatchFunc func, gpointer data); + gint key; +}; + static void target_free(ObtWatchTarget *t); +static void target_notify(const gchar *const path, gpointer target, + ObtWatchNotifyType type); ObtWatch* obt_watch_new() { ObtWatch *w; - - w = g_slice_new(ObtWatch); - w->ref = 1; - w->ino_fd = -1; - w->targets = g_hash_table_new_full(g_str_hash, g_str_equal, - NULL, (GDestroyNotify)target_free); -#ifdef HAVE_SYS_INOTIFY_H - w->targets_by_wd = g_hash_table_new(g_int_hash, g_int_equal); -#endif - - init_inot(w); - + GSource *source; + + w = NULL; + source = watch_sys_create_source(target_notify); + if (source) { + w = g_slice_new(ObtWatch); + w->ref = 1; + w->targets_by_path = g_hash_table_new_full( + g_str_hash, g_str_equal, NULL, (GDestroyNotify)target_free); + w->source = source; + } return w; } + void obt_watch_ref(ObtWatch *w) { ++w->ref; @@ -83,155 +99,64 @@ void obt_watch_ref(ObtWatch *w) void obt_watch_unref(ObtWatch *w) { if (--w->ref < 1) { - if (w->ino_fd >= 0 && w->ino_watch) - g_source_remove(w->ino_watch); - - g_hash_table_destroy(w->targets); -#ifdef HAVE_SYS_INOTIFY_H - g_hash_table_destroy(w->targets_by_wd); -#endif - + g_hash_table_destroy(w->targets_by_path); + g_source_destroy(w->source); g_slice_free(ObtWatch, w); } } -static void init_inot(ObtWatch *w) +static void target_free(ObtWatchTarget *t) { -#ifdef HAVE_SYS_INOTIFY_H - if (w->ino_fd >= 0) return; - - w->ino_fd = inotify_init(); - if (w->ino_fd >= 0) { - GIOChannel *ch; - - ch = g_io_channel_unix_new(w->ino_fd); - w->ino_watch = g_io_add_watch(ch, G_IO_IN | G_IO_HUP | G_IO_ERR, - read_inot, w); - g_io_channel_unref(ch); - } -#endif + if (t->key >= 0) + watch_sys_remove_target(t->w->source, t->key); + g_free(t->base_path); + g_slice_free(ObtWatchTarget, t); } -static gboolean read_inot(GIOChannel *src, GIOCondition cond, gpointer data) +gboolean obt_watch_add(ObtWatch *w, const gchar *path, + gboolean watch_hidden, + ObtWatchFunc func, gpointer data) { -#ifdef HAVE_SYS_INOTIFY_H - ObtWatch *w = data; ObtWatchTarget *t; - struct inotify_event s; - gint len; - guint ilen; - char *name; - - /* read the event */ - for (ilen = 0; ilen < sizeof(s); ilen += len) { - len = read(w->ino_fd, ((char*)&s)+ilen, sizeof(s)-ilen); - if (len < 0 && errno != EINTR) return FALSE; /* error, don't repeat */ - if (!len) return TRUE; /* nothing there */ - } - name = g_new(char, s.len); - - /* read the filename */ - for (ilen = 0; ilen < s.len; ilen += len) { - len = read(w->ino_fd, name+ilen, s.len-ilen); - if (len < 0 && errno != EINTR) return FALSE; /* error, don't repeat */ - if (!len) return TRUE; /* nothing there */ - } - - t = g_hash_table_lookup(w->targets, &s.wd); - if (t) t->func(w, name, t->data); - - g_free(name); -#endif - return TRUE; /* repeat */ -} - -static gboolean add_inot(ObtWatch *w, ObtWatchTarget *t, const char *path, - gboolean dir) -{ -#ifndef HAVE_SYS_INOTIFY_H - return FALSE; -#else - gint mask; - if (w->ino_fd < 0) return FALSE; - if (dir) mask = IN_MODIFY | IN_CREATE | IN_DELETE | IN_MOVE; - else g_assert_not_reached(); - t->wd = inotify_add_watch(w->ino_fd, path, mask); - return TRUE; -#endif -} - -static void rm_inot(ObtWatchTarget *t) -{ -#ifdef HAVE_SYS_INOTIFY_H - if (t->w->ino_fd < 0) return; - if (t->wd < 0) return; - inotify_rm_watch(t->w->ino_fd, t->wd); -#endif -} - -static ObtWatchTarget* target_new(ObtWatch *w, const gchar *path, - ObtWatchFunc func, gpointer data) -{ - ObtWatchTarget *t; + g_return_val_if_fail(w != NULL, FALSE); + g_return_val_if_fail(path != NULL, FALSE); + g_return_val_if_fail(func != NULL, FALSE); + g_return_val_if_fail(path[0] == G_DIR_SEPARATOR, FALSE); t = g_slice_new0(ObtWatchTarget); t->w = w; -#ifdef HAVE_SYS_INOTIFY_H - t->wd = -1; -#endif - t->path = g_strdup(path); + t->base_path = g_strdup(path); t->func = func; t->data = data; + g_hash_table_insert(w->targets_by_path, t->base_path, t); - if (!add_inot(w, t, path, TRUE)) { - g_assert_not_reached(); /* XXX do something */ + t->key = watch_sys_add_target(w->source, path, watch_hidden, t); + if (t->key < 0) { + g_hash_table_remove(w->targets_by_path, t->base_path); + return FALSE; } -#ifndef HAVE_SYS_INOTIFY_H -#error need inotify for now -#endif - - return t; -} - -static void target_free(ObtWatchTarget *t) -{ - rm_inot(t); - - g_free(t->path); - g_slice_free(ObtWatchTarget, t); + return TRUE; } -void obt_paths_watch_dir(ObtWatch *w, const gchar *path, - ObtWatchFunc func, gpointer data) +void obt_watch_remove(ObtWatch *w, const gchar *path) { - ObtWatchTarget *t; - g_return_if_fail(w != NULL); g_return_if_fail(path != NULL); - g_return_if_fail(data != NULL); + g_return_if_fail(path[0] == G_DIR_SEPARATOR); - t = target_new(w, path, func, data); - g_hash_table_insert(w->targets, t->path, t); -#ifdef HAVE_SYS_INOTIFY_H - g_hash_table_insert(w->targets_by_wd, &t->wd, t); -#endif + /* this also calls target_free */ + g_hash_table_remove(w->targets_by_path, path); } -void obt_paths_unwatch_dir(ObtWatch *w, const gchar *path) +static void target_notify(const gchar *const path, gpointer target, + ObtWatchNotifyType type) { - ObtWatchTarget *t; - - g_return_if_fail(w != NULL); - g_return_if_fail(path != NULL); - - t = g_hash_table_lookup(w->targets, path); - - if (t) { -#ifdef HAVE_SYS_INOTIFY_H - g_hash_table_remove(w->targets_by_wd, &t->wd); -#endif - g_hash_table_remove(w->targets, path); + ObtWatchTarget *t = target; + if (type == OBT_WATCH_SELF_REMOVED) { + /* this also calls target_free */ + g_hash_table_remove(t->w->targets_by_path, t->base_path); } + t->func(t->w, path, type, t->data); } diff --git a/obt/watch.h b/obt/watch.h index c8556bc0..d46e4f82 100644 --- a/obt/watch.h +++ b/obt/watch.h @@ -24,17 +24,37 @@ G_BEGIN_DECLS typedef struct _ObtWatch ObtWatch; +typedef enum _ObtWatchNotifyType ObtWatchNotifyType; -struct _ObtMainLoop; +typedef void (*ObtWatchFunc)(ObtWatch *w, const gchar *subpath, + ObtWatchNotifyType type, gpointer data); -typedef void (*ObtWatchFunc)(ObtWatch *w, gchar *subpath, gpointer data); +enum _ObtWatchNotifyType { + OBT_WATCH_ADDED, /*!< A file/dir was added in a watched dir */ + OBT_WATCH_REMOVED, /*!< A file/dir was removed in a watched dir */ + OBT_WATCH_MODIFIED, /*!< A watched file, or a file in a watched dir, was + modified */ + OBT_WATCH_SELF_REMOVED /*!< The watched target was removed. */ +}; ObtWatch* obt_watch_new(); void obt_watch_ref(ObtWatch *w); void obt_watch_unref(ObtWatch *w); -void obt_watch_dir(ObtWatch *w, const gchar *path, - ObtWatchFunc func, gpointer data); +/*! Start watching a target file or directory. + If the target is a directory, the watch is performed recursively. + On start, if the target is a directory, an ADDED notification will come for + each file in the directory, and its subdirectories. + @param path The path to the target to watch. Must be an absolute path that + starts with a /. + @param watch_hidden If TRUE, and if the target is a directory, dot-files (and + dot-subdirectories) will be included in the watch. If the target is a + file, this parameter is ignored. +*/ +gboolean obt_watch_add(ObtWatch *w, const gchar *path, + gboolean watch_hidden, + ObtWatchFunc func, gpointer data); +void obt_watch_remove(ObtWatch *w, const gchar *path); G_END_DECLS diff --git a/obt/watch_inotify.c b/obt/watch_inotify.c new file mode 100644 index 00000000..3a7d3da4 --- /dev/null +++ b/obt/watch_inotify.c @@ -0,0 +1,396 @@ +/* -*- indent-tabs-mode: nil; tab-width: 4; c-basic-offset: 4; -*- + + obt/watch.c for the Openbox window manager + Copyright (c) 2010 Dana Jansens + + 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. + + See the COPYING file for a copy of the GNU General Public License. +*/ + +#ifdef HAVE_SYS_INOTIFY_H + +#include "watch.h" + +#include +#ifdef HAVE_UNISTD_H +# include +#endif +#ifdef HAVE_STRING_H +# include +#endif +#ifdef HAVE_ERRNO_H +# include +#endif + +#include + +typedef struct _InoSource InoSource; +typedef struct _InoTarget InoTarget; + +/*! Callback function in the watch general system. + Matches definition in watch.c +*/ +typedef void (*ObtWatchNotifyFunc)(const gchar *path, gpointer target, + ObtWatchNotifyType type); + +struct _InoSource { + GSource source; + + GPollFD pfd; + ObtWatchNotifyFunc notify; + GHashTable *targets_by_key; + GHashTable *targets_by_path; +}; + +struct _InoTarget { + gint key; + gchar *path; + gpointer watch_target; + gboolean is_dir; + gboolean watch_hidden; +}; + +static gboolean source_check(GSource *source); +static gboolean source_prepare(GSource *source, gint *timeout); +static gboolean source_read(GSource *source, GSourceFunc cb, gpointer data); +static void source_finalize(GSource *source); +static gint add_target(GSource *source, InoTarget *parent, + const gchar *path, gboolean watch_hidden, + gpointer target); +static void remove_target(GSource *source, InoTarget *target); +static void target_free(InoTarget *target); + +static GSourceFuncs source_funcs = { + source_prepare, + source_check, + source_read, + source_finalize +}; + +gint watch_sys_add_target(GSource *source, const char *path, + gboolean watch_hidden, gpointer target) +{ + return add_target(source, NULL, path, watch_hidden, target); +} + +void watch_sys_remove_target(GSource *source, gint key) +{ + InoSource *ino_source = (InoSource*)source; + InoTarget *t; + + t = g_hash_table_lookup(ino_source->targets_by_key, &key); + remove_target(source, t); +} + +GSource* watch_sys_create_source(ObtWatchNotifyFunc notify) +{ + gint fd; + GSource *source; + InoSource *ino_source; + + g_return_val_if_fail(notify != NULL, NULL); + + source = NULL; + fd = inotify_init(); + if (fd < 0) { + gchar *s = strerror(errno); + g_warning("Failed to initialize inotify: %s", s); + } + else { + g_debug("initialized inotify on fd %d", fd); + source = g_source_new(&source_funcs, sizeof(InoSource)); + ino_source = (InoSource*)source; + ino_source->notify = notify; + ino_source->targets_by_key = g_hash_table_new_full( + g_int_hash, g_int_equal, NULL, NULL); + ino_source->targets_by_path = g_hash_table_new_full( + g_str_hash, g_str_equal, NULL, (GDestroyNotify)target_free); + ino_source->pfd = (GPollFD){ fd, G_IO_IN, G_IO_IN }; + g_source_add_poll(source, &ino_source->pfd); + g_source_attach(source, NULL); + } + return source; +} + +static gboolean source_prepare(GSource *source, gint *timeout) +{ + *timeout = -1; + return FALSE; +} + +static gboolean source_check(GSource *source) +{ + return TRUE; +} + +static gboolean source_read(GSource *source, GSourceFunc cb, gpointer data) +{ + const gint BUF_LEN = sizeof(struct inotify_event) + 1024; + InoSource *ino_source = (InoSource*)source; + gchar buffer[BUF_LEN]; + gchar *name; + guint name_len; /* number of bytes read for the name */ + guint event_len; /* number of bytes read for the event */ + gint len; /* number of bytes in the buffer */ + gint pos; /* position in the buffer */ + enum { + READING_EVENT, + READING_NAME_BUFFER, + READING_NAME_HEAP + } state; + struct inotify_event event; + + pos = BUF_LEN; + state = READING_EVENT; + len = event_len = name_len = 0; + + while (TRUE) { + if (pos == len || !len || event_len) { + /* refill the buffer */ + + if (event_len) + pos = event_len; + else + pos = 0; + + len = read(ino_source->pfd.fd, &buffer[pos], BUF_LEN-pos); + + if (len < 0 && errno != EINTR) { + gchar *s = strerror(errno); + g_warning("Failed to read from inotify: %s", s); + return FALSE; /* won't read any more */ + } + if (len == 0) { + g_debug("Done reading from inotify"); + return TRUE; + } + + g_debug("read %d bytes", len); + } + + if (state == READING_EVENT) { + const guint remain = len - pos; + + if (remain < sizeof(struct inotify_event)) { + /* there is more of the event struct to read */ + guint i; + for (i = 0; i < remain; ++i) + buffer[i] = buffer[pos+i]; + g_debug("leftover %d bytes of event struct", remain); + } + else { + event = *(struct inotify_event*)&buffer[pos]; + pos += sizeof(struct inotify_event); + + g_debug("read event: wd %d mask %x len %d", + event.wd, event.mask, event.len); + + if (remain >= event.len) { + g_debug("name fits in buffer"); + state = READING_NAME_BUFFER; + name = &buffer[pos]; + name_len = event.len; + pos += event.len; + } + else { /* remain < event.len */ + g_debug("name doesn't fit in buffer"); + state = READING_NAME_HEAP; + name = g_new(gchar, event.len); + memcpy(name, &buffer[pos], remain); + name_len = remain; + pos += remain; + } + } + } + if (state == READING_NAME_HEAP && pos < len) { + const guint buf_remain = len - pos; + const guint name_remain = event.len - name_len; + const guint copy_len = MIN(buf_remain, name_remain); + memcpy(name+name_len, &buffer[pos], copy_len); + name_len += copy_len; + pos += copy_len; + } + if ((state == READING_NAME_BUFFER || state == READING_NAME_HEAP) && + name_len == event.len) + { + /* done reading the file name ! */ + InoTarget *t; + gboolean report; + ObtWatchNotifyType type; + gchar *full_path; + + g_debug("read filename %s mask %x", name, event.mask); + + event.mask &= ~IN_IGNORED; /* skip this one, we watch for things + to get removed explicitly so this + will just be double-reporting */ + if (event.mask) { + + t = g_hash_table_lookup(ino_source->targets_by_key, &event.wd); + g_assert(t != NULL); + + full_path = g_build_filename(t->path, name, NULL); + g_debug("full path to change: %s", full_path); + + /* don't report hidden stuff inside a directory watch */ + report = !t->is_dir || name[0] != '.' || t->watch_hidden; + if (event.mask & IN_MOVE_SELF) { + g_warning("Watched target was moved away: %s", t->path); + type = OBT_WATCH_SELF_REMOVED; + } + else if (event.mask & IN_ISDIR) { + if (event.mask & IN_MOVED_TO || + event.mask & IN_CREATE) + { + add_target(source, t, full_path, t->watch_hidden, + t->watch_target); + g_debug("added %s", full_path); + } + else if (event.mask & IN_MOVED_FROM || + event.mask & IN_DELETE) + { + InoTarget *subt; + + subt = g_hash_table_lookup(ino_source->targets_by_path, + full_path); + g_assert(subt); + remove_target(source, subt); + g_debug("removed %s", full_path); + } + report = FALSE; + } + else { + if (event.mask & IN_MOVED_TO || event.mask & IN_CREATE) + type = OBT_WATCH_ADDED; + else if (event.mask & IN_MOVED_FROM || + event.mask & IN_DELETE) + type = OBT_WATCH_REMOVED; + else if (event.mask & IN_MODIFY) + type = OBT_WATCH_MODIFIED; + else + g_assert_not_reached(); + } + + if (report) { + /* call the GSource callback if there is one */ + if (cb) cb(data); + + /* call the WatchNotify callback */ + ino_source->notify(full_path, t->watch_target, type); + } + + g_free(full_path); + } + + if (state == READING_NAME_HEAP) + g_free(name); + state = READING_EVENT; + } + } +} + +static void source_finalize(GSource *source) +{ + InoSource *ino_source = (InoSource*)source; + g_debug("source_finalize"); + close(ino_source->pfd.fd); + g_hash_table_destroy(ino_source->targets_by_key); +} + +static gint add_target(GSource *source, InoTarget *parent, + const gchar *path, gboolean watch_hidden, + gpointer target) +{ + InoSource *ino_source; + InoTarget *ino_target; + guint32 mask; + gint key; + gboolean is_dir; + + ino_source = (InoSource*)source; + + is_dir = g_file_test(path, G_FILE_TEST_IS_DIR); + if (is_dir) + mask = IN_MODIFY | IN_CREATE | IN_DELETE | IN_MOVE; + else + mask = IN_MODIFY; + /* only watch IN_MOVE_SELF on the top-most target of the watch */ + if (!parent) + mask |= IN_MOVE_SELF; + + ino_target = NULL; + key = inotify_add_watch(ino_source->pfd.fd, path, mask); + g_debug("added watch descriptor %d for fd %d on path %s", + key, ino_source->pfd.fd, path); + if (key < 0) { + gchar *s = strerror(errno); + g_warning("Unable to watch path %s: %s", path, s); + } + else { + ino_target = g_slice_new(InoTarget); + ino_target->key = key; + ino_target->path = g_strdup(path); + ino_target->is_dir = is_dir; + ino_target->watch_hidden = watch_hidden; + ino_target->watch_target = target; + g_hash_table_insert(ino_source->targets_by_key, &ino_target->key, + ino_target); + g_hash_table_insert(ino_source->targets_by_path, ino_target->path, + ino_target); + } + + if (key >= 0 && is_dir) { + /* recurse */ + GDir *dir; + + dir = g_dir_open(path, 0, NULL); + if (dir) { + const gchar *name; + + while ((name = g_dir_read_name(dir))) { + if (name[0] != '.' || watch_hidden) { + gchar *subpath; + + subpath = g_build_filename(path, name, NULL); + if (g_file_test(subpath, G_FILE_TEST_IS_DIR)) + add_target(source, ino_target, subpath, watch_hidden, + target); + else + /* notify for each file in the directory on startup */ + ino_source->notify(subpath, ino_target->watch_target, + OBT_WATCH_ADDED); + g_free(subpath); + } + } + } + g_dir_close(dir); + } + + return key; +} + +static void remove_target(GSource *source, InoTarget *target) +{ + InoSource *ino_source = (InoSource*)source; + g_debug("removing wd %d for fd %d", target->key, ino_source->pfd.fd); + inotify_rm_watch(ino_source->pfd.fd, (guint32)target->key); + g_hash_table_remove(ino_source->targets_by_key, &target->key); + g_hash_table_remove(ino_source->targets_by_path, target->path); +} + +static void target_free(InoTarget *target) +{ + g_free(target->path); + g_slice_free(InoTarget, target); +} + +#endif