Add/fix inotify support for watching filesystem changes.
authorDana Jansens <danakj@orodu.net>
Mon, 20 Sep 2010 19:45:36 +0000 (15:45 -0400)
committerDana Jansens <danakj@orodu.net>
Sun, 16 Oct 2011 22:54:04 +0000 (18:54 -0400)
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.

Makefile.am
obt/watch.c
obt/watch.h
obt/watch_inotify.c [new file with mode: 0644]

index c4c9057ed8c5d73b03395829ba2b81a201c4b3c8..426fe1ee28043d8809c42bbbf993d00822c0a6ce 100644 (file)
@@ -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
 
index 3ed2d954faaa1d1e5be5176e3b2350479d9f2b6a..63a2ccef7771091fcef36e27ae2950e4fe0f205b 100644 (file)
 
 #include "obt/watch.h"
 
-#ifdef HAVE_SYS_INOTIFY_H
-#  include <sys/inotify.h>
-#endif
 #ifdef HAVE_UNISTD_H
 #  include <unistd.h>
 #endif
-#include <errno.h>
+
+#include <glib.h>
+
+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_<system>.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);
 }
index c8556bc06e9781046883f19ae4542fed4fdfb41e..d46e4f82a376211ebdf689cfb42ab0fe87e862f4 100644 (file)
 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 (file)
index 0000000..3a7d3da
--- /dev/null
@@ -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 <sys/inotify.h>
+#ifdef HAVE_UNISTD_H
+#  include <unistd.h>
+#endif
+#ifdef HAVE_STRING_H
+#  include <string.h>
+#endif
+#ifdef HAVE_ERRNO_H
+#  include <errno.h>
+#endif
+
+#include <glib.h>
+
+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