/* * gam_kqueue.c - a kqueue(2) Gamin backend * * Notes: * * * http://techpubs.sgi.com/library/tpl/cgi-bin/getdoc.cgi?coll=0650&db=bks&fname=/SGI_Developer/books/IIDsktp_IG/sgi_html/ch08.html * states that FAM does not follow monitored symbolic links: we * do the same (note that regarding * http://oss.sgi.com/bugzilla/show_bug.cgi?id=405, we do what * FAM should do: we do not call g_dir_open() if the file is a * symbolic link). * * * While kqueue is no longer tied to the UFS file system, it is * better to not use it for remote file systems (because for * such file systems, only local changes are detected by * the kernel). * * * Monitoring a file with kqueue prevents the file system it * resides on from being unmounted, because kqueue can only * monitor open files. * * * The creation of missing monitored files is detected by * performing a lstat() every second. Although using kqueue to * monitor the parent directory is technically feasible, it * would introduce a lot of complexity in the code. * * * In light of the previous points, it appears that: * * - kqueue needs to be augmented with a filename-based * monitoring facility; * * Copyright (C) 2005 Joe Marcus Clarke * Copyright (C) 2005, 2006 Jean-Yves Lefort * Copyright (C) 2006 Alex Dupre * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free * Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */ #include "config.h" #include #include #include #include #ifndef USE_GAMIN_POLLER #include #endif #include #include #include #include #include #include #include "gam_error.h" #include "gam_kqueue.h" #include "gam_event.h" #include "gam_server.h" #include "gam_poll_basic.h" /*** tunable constants, modify to tweak the backend aggressivity *************/ /* * The backend will use at most n file descriptors, where n is the * minimum value between (kern.maxfiles * CFG_GLOBAL_FILE_RESERVE_RATIO) * and (kern.maxfilesperproc - CFG_SELF_FILE_RESERVE). */ #define CFG_GLOBAL_FILE_RESERVE_RATIO 0.7 #define CFG_SELF_FILE_RESERVE 200 /* * If a SubMonitor or FileMonitor is not supported by kqueue and has * to be polled, the backend will re-attempt to enable kqueue * notification every n polls. */ #define CFG_UNSUPPORTED_SMON_KQUEUE_RETRY_FREQUENCY 10 #define CFG_UNSUPPORTED_FMON_KQUEUE_RETRY_FREQUENCY 10 /* * The various poll intervals, in milliseconds. The default interval * for each poller is based on the poller's expected usage. */ #define CFG_MISSING_SMON_POLL_INTERVAL 1000 #define CFG_UNSUPPORTED_SMON_POLL_INTERVAL 3000 #define CFG_UNSUPPORTED_FMON_POLL_INTERVAL 3000 /*** end of tunable constants ************************************************/ #define VN_NOTE_CHANGED (NOTE_WRITE | NOTE_EXTEND | NOTE_ATTRIB | NOTE_LINK) #define VN_NOTE_DELETED (NOTE_DELETE | NOTE_REVOKE) #define VN_NOTE_ALL (VN_NOTE_CHANGED | VN_NOTE_DELETED | NOTE_RENAME) /* * A barebone stat structure, only containing the fields we need. */ typedef struct { gboolean exists; ino_t ino; mode_t mode; uid_t uid; gid_t gid; time_t mtime; time_t ctime; off_t size; } MiniStat; typedef void (*HashTableAddFunc) (GHashTable *table, gpointer item, gpointer user_data); typedef void (*HashTablePostAddFunc) (GHashTable *table, gpointer user_data); typedef void (*HashTableRemoveFunc) (GHashTable *table, gpointer item, gpointer user_data); typedef void (*HashTablePostRemoveFunc) (GHashTable *table, gpointer user_data); typedef struct { HashTableAddFunc add; HashTablePostAddFunc post_add; HashTableRemoveFunc remove; HashTablePostRemoveFunc post_remove; } HashTableMethods; /* * A hash table which can be modified while iterating over it. */ typedef struct { GHashTable *main; gboolean iterating; GSList *pending_additions; GSList *pending_removals; HashTableMethods *methods; gpointer user_data; } HashTable; /* the base monitor class */ typedef struct _Monitor Monitor; struct _Monitor { void (*handle_kevent) (Monitor *monitor, struct kevent *event); char *pathname; int fd; /* for kqueue */ MiniStat sb; /* for poll */ unsigned int poll_count; }; #define MONITOR(ptr) ((Monitor *) ptr) typedef enum { MONITOR_ISDIR = 1 << 0, MONITOR_ISNOTDIR = 1 << 1 } MonitorFlags; #define MONITOR_FLAGS_SHIFT 2 /* monitor for Gamin subscriptions */ typedef struct { Monitor base; GList *subs; HashTable *fmons; /* FileMonitor objects */ HashTable *unsupported_fmons; /* subset of fmons to poll */ gboolean isdir; /* is a directory subscription? */ } SubMonitor; #define SUB_MONITOR(ptr) ((SubMonitor *) ptr) typedef enum { SUB_MONITOR_WAS_MISSING = 1 << MONITOR_FLAGS_SHIFT } SubMonitorFlags; /* monitor for files within directory subscriptions */ typedef struct { Monitor base; SubMonitor *smon; /* the SubMonitor this fmon belongs to */ char *filename; /* pointer into base.pathname */ } FileMonitor; #define FILE_MONITOR(ptr) ((FileMonitor *) ptr) typedef enum { FILE_MONITOR_POSSIBLY_RECREATED = 1 << MONITOR_FLAGS_SHIFT } FileMonitorFlags; typedef void (*PollerFunc) (SubMonitor *smon); typedef struct { PollerFunc func; unsigned int interval; unsigned int timeout_id; HashTable *smons; /* SubMonitor objects */ } Poller; static int kq = -1; static unsigned int open_files = 0; static unsigned int max_open_files = 0; static GHashTable *dir_hash = NULL; static GHashTable *file_hash = NULL; static Poller missing_smon_poller; static Poller unsupported_smon_poller; static Poller unsupported_fmon_poller; static void gam_kqueue_hash_table_default_add_cb (GHashTable *table, gpointer item, gpointer user_data); static void gam_kqueue_hash_table_default_remove_cb (GHashTable *table, gpointer item, gpointer user_data); static void gam_kqueue_poller_post_add_cb (GHashTable *table, Poller *poller); static void gam_kqueue_poller_post_remove_cb (GHashTable *table, Poller *poller); static HashTableMethods poller_hash_table_methods = { gam_kqueue_hash_table_default_add_cb, (HashTablePostAddFunc) gam_kqueue_poller_post_add_cb, gam_kqueue_hash_table_default_remove_cb, (HashTablePostRemoveFunc) gam_kqueue_poller_post_remove_cb }; static void gam_kqueue_sub_monitor_add_fmon_cb (GHashTable *table, FileMonitor *fmon, SubMonitor *smon); static void gam_kqueue_sub_monitor_remove_fmon_cb (GHashTable *table, FileMonitor *fmon, SubMonitor *smon); static HashTableMethods sub_monitor_fmons_hash_table_methods = { (HashTableAddFunc) gam_kqueue_sub_monitor_add_fmon_cb, (HashTablePostAddFunc) NULL, (HashTableRemoveFunc) gam_kqueue_sub_monitor_remove_fmon_cb, (HashTablePostRemoveFunc) NULL }; static void gam_kqueue_sub_monitor_post_add_unsupported_fmon_cb (GHashTable *table, SubMonitor *smon); static void gam_kqueue_sub_monitor_post_remove_unsupported_fmon_cb (GHashTable *table, SubMonitor *smon); static HashTableMethods sub_monitor_unsupported_fmons_hash_table_methods = { gam_kqueue_hash_table_default_add_cb, (HashTablePostAddFunc) gam_kqueue_sub_monitor_post_add_unsupported_fmon_cb, gam_kqueue_hash_table_default_remove_cb, (HashTablePostRemoveFunc) gam_kqueue_sub_monitor_post_remove_unsupported_fmon_cb }; static void gam_kqueue_sub_monitor_emit_event (SubMonitor *smon, GaminEventType event, SubMonitorFlags flags); static void gam_kqueue_sub_monitor_handle_kevent (Monitor *mon, struct kevent *event); static FileMonitor *gam_kqueue_file_monitor_new (SubMonitor *smon, const char *filename, FileMonitorFlags *flags); static void gam_kqueue_file_monitor_free (FileMonitor *fmon); static void gam_kqueue_file_monitor_emit_event (FileMonitor *fmon, GaminEventType event, FileMonitorFlags flags); static void gam_kqueue_file_monitor_handle_kevent (Monitor *mon, struct kevent *event); /*** helpers *****************************************************************/ static void gam_kqueue_mini_lstat (const char *pathname, MiniStat *mini_sb) { struct stat sb; if (lstat(pathname, &sb) < 0) memset(mini_sb, 0, sizeof(*mini_sb)); else { mini_sb->exists = TRUE; mini_sb->ino = sb.st_ino; mini_sb->mode = sb.st_mode; mini_sb->uid = sb.st_uid; mini_sb->gid = sb.st_gid; mini_sb->mtime = sb.st_mtime; mini_sb->ctime = sb.st_ctime; mini_sb->size = sb.st_size; } } static gboolean gam_kqueue_differs (const MiniStat *sb1, const MiniStat *sb2) { return sb1->mtime != sb2->mtime || sb1->ctime != sb2->ctime || sb1->size != sb2->size || sb1->mode != sb2->mode || sb1->uid != sb2->uid || sb1->gid != sb2->gid || sb1->ino != sb2->ino; } static gboolean gam_kqueue_isdir (const char *pathname, MonitorFlags flags) { if ((flags & MONITOR_ISDIR) != 0) return TRUE; else if ((flags & MONITOR_ISNOTDIR) != 0) return FALSE; else { struct stat sb; return lstat(pathname, &sb) >= 0 && (sb.st_mode & S_IFDIR) != 0; } } static gboolean gam_kqueue_get_uint_sysctl (const char *name, unsigned int *value) { size_t value_len = sizeof(*value); if (sysctlbyname(name, value, &value_len, (void *)NULL, 0) < 0) { gam_error(DEBUG_INFO, "unable to retrieve %s: %s\n", name, g_strerror(errno)); return FALSE; } else return TRUE; } /*** HashTable ***************************************************************/ static HashTable * gam_kqueue_hash_table_new (GHashTable *main, HashTableMethods *methods, gpointer user_data) { HashTable *table; table = g_new0(HashTable, 1); table->main = main; table->methods = methods; table->user_data = user_data; return table; } static void gam_kqueue_hash_table_default_add_cb (GHashTable *table, gpointer item, gpointer user_data) { g_hash_table_insert(table, item, GINT_TO_POINTER(TRUE)); } static void gam_kqueue_hash_table_default_remove_cb (GHashTable *table, gpointer item, gpointer user_data) { g_hash_table_remove(table, item); } static void gam_kqueue_hash_table_add (HashTable *table, gpointer item) { if (table->iterating) table->pending_additions = g_slist_prepend(table->pending_additions, item); else { table->methods->add(table->main, item, table->user_data); if (table->methods->post_add) table->methods->post_add(table->main, table->user_data); } } static void gam_kqueue_hash_table_remove (HashTable *table, gpointer item) { if (table->iterating) table->pending_removals = g_slist_prepend(table->pending_removals, item); else { table->methods->remove(table->main, item, table->user_data); if (table->methods->post_remove) table->methods->post_remove(table->main, table->user_data); } } static void gam_kqueue_hash_table_foreach (HashTable *table, GHFunc func, gpointer user_data) { g_assert(table->iterating == FALSE); table->iterating = TRUE; g_hash_table_foreach(table->main, func, user_data); table->iterating = FALSE; if (table->pending_additions) { GSList *l; for (l = table->pending_additions; l != NULL; l = l->next) table->methods->add(table->main, l->data, table->user_data); g_slist_free(table->pending_additions); table->pending_additions = NULL; if (table->methods->post_add) table->methods->post_add(table->main, table->user_data); } if (table->pending_removals) { GSList *l; for (l = table->pending_removals; l != NULL; l = l->next) table->methods->remove(table->main, l->data, table->user_data); g_slist_free(table->pending_removals); table->pending_removals = NULL; if (table->methods->post_remove) table->methods->post_remove(table->main, table->user_data); } } static void gam_kqueue_hash_table_destroy (HashTable *table) { g_assert(table->iterating == FALSE); g_hash_table_destroy(table->main); g_free(table); } /*** Poller ******************************************************************/ static void gam_kqueue_poller_init (Poller *poller, PollerFunc func, unsigned int interval) { poller->func = func; poller->interval = interval; poller->timeout_id = 0; poller->smons = gam_kqueue_hash_table_new(g_hash_table_new(g_direct_hash, g_direct_equal), &poller_hash_table_methods, poller); } static void gam_kqueue_poller_foreach_cb (SubMonitor *smon, gpointer unused, Poller *poller) { poller->func(smon); } static gboolean gam_kqueue_poller_timeout_cb (Poller *poller) { gam_kqueue_hash_table_foreach(poller->smons, (GHFunc) gam_kqueue_poller_foreach_cb, poller); return TRUE; /* keep source */ } static void gam_kqueue_poller_post_add_cb (GHashTable *table, Poller *poller) { if (! poller->timeout_id) poller->timeout_id = g_timeout_add(poller->interval, (GSourceFunc) gam_kqueue_poller_timeout_cb, poller); } static void gam_kqueue_poller_post_remove_cb (GHashTable *table, Poller *poller) { if (g_hash_table_size(table) == 0 && poller->timeout_id) { g_source_remove(poller->timeout_id); poller->timeout_id = 0; } } static void gam_kqueue_poller_add_sub_monitor (Poller *poller, SubMonitor *smon) { gam_kqueue_hash_table_add(poller->smons, smon); } static void gam_kqueue_poller_remove_sub_monitor (Poller *poller, SubMonitor *smon) { gam_kqueue_hash_table_remove(poller->smons, smon); } /*** Monitor *****************************************************************/ static gboolean gam_kqueue_monitor_enable_kqueue (Monitor *mon) { struct kevent ev[1]; #ifndef USE_GAMIN_POLLER struct statfs sb; #endif if (open_files == max_open_files) { GAM_DEBUG(DEBUG_INFO, "cannot open %s (max_open_files limit reached), falling back to poll\n", mon->pathname); return FALSE; } mon->fd = open(mon->pathname, O_RDONLY | O_NONBLOCK | O_NOFOLLOW); if (mon->fd < 0) { GAM_DEBUG(DEBUG_INFO, "cannot open %s (%s), falling back to poll\n", mon->pathname, g_strerror(errno)); return FALSE; } #ifndef USE_GAMIN_POLLER if (fstatfs(mon->fd, &sb) == 0 && (sb.f_flags & MNT_LOCAL) == 0) { GAM_DEBUG(DEBUG_INFO, "%s resides on a remote file system, falling back to poll\n", mon->pathname); goto poll; } #endif EV_SET(ev, mon->fd, EVFILT_VNODE, EV_ADD | EV_ENABLE | EV_CLEAR, VN_NOTE_ALL, 0, mon); if (kevent(kq, ev, G_N_ELEMENTS(ev), NULL, 0, NULL) < 0) { GAM_DEBUG(DEBUG_INFO, "cannot enable kqueue notification for %s (%s), falling back to poll\n", mon->pathname, g_strerror(errno)); goto poll; } open_files++; return TRUE; poll: close(mon->fd); mon->fd = -1; return FALSE; } static void gam_kqueue_monitor_set_unsupported (Monitor *mon) { if (mon->fd >= 0) { close(mon->fd); mon->fd = -1; open_files--; } gam_kqueue_mini_lstat(mon->pathname, &mon->sb); } static void gam_kqueue_monitor_free (Monitor *mon) { g_free(mon->pathname); if (mon->fd >= 0) { close(mon->fd); open_files--; } g_free(mon); } /*** SubMonitor **************************************************************/ static void gam_kqueue_sub_monitor_init_fmons (SubMonitor *smon) { smon->fmons = gam_kqueue_hash_table_new(g_hash_table_new_full(g_str_hash, g_str_equal, NULL, (GDestroyNotify) gam_kqueue_file_monitor_free), &sub_monitor_fmons_hash_table_methods, smon); smon->unsupported_fmons = gam_kqueue_hash_table_new(g_hash_table_new(g_direct_hash, g_direct_equal), &sub_monitor_unsupported_fmons_hash_table_methods, smon); } static SubMonitor * gam_kqueue_sub_monitor_new (GamSubscription *sub) { SubMonitor *smon; Monitor *mon; smon = g_new0(SubMonitor, 1); mon = MONITOR(smon); mon->handle_kevent = gam_kqueue_sub_monitor_handle_kevent; mon->pathname = g_strdup(gam_subscription_get_path(sub)); mon->fd = -1; smon->isdir = gam_subscription_is_dir(sub); gam_kqueue_sub_monitor_init_fmons(smon); return smon; } static void gam_kqueue_sub_monitor_clear_fmons (SubMonitor *smon) { if (g_hash_table_size(smon->unsupported_fmons->main) > 0) gam_kqueue_poller_remove_sub_monitor(&unsupported_fmon_poller, smon); gam_kqueue_hash_table_destroy(smon->unsupported_fmons); gam_kqueue_hash_table_destroy(smon->fmons); } static void gam_kqueue_sub_monitor_free (SubMonitor *smon) { gam_kqueue_poller_remove_sub_monitor(&missing_smon_poller, smon); gam_kqueue_poller_remove_sub_monitor(&unsupported_smon_poller, smon); /* unsupported_dirs_poller is handled by _clear_fmons() below */ gam_kqueue_sub_monitor_clear_fmons(smon); gam_kqueue_monitor_free(MONITOR(smon)); } static void gam_kqueue_sub_monitor_add_fmon_cb (GHashTable *table, FileMonitor *fmon, SubMonitor *smon) { g_hash_table_replace(table, fmon->filename, fmon); } static void gam_kqueue_sub_monitor_remove_fmon_cb (GHashTable *table, FileMonitor *fmon, SubMonitor *smon) { g_hash_table_remove(table, fmon->filename); } static void gam_kqueue_sub_monitor_post_add_unsupported_fmon_cb (GHashTable *table, SubMonitor *smon) { gam_kqueue_poller_add_sub_monitor(&unsupported_fmon_poller, smon); } static void gam_kqueue_sub_monitor_post_remove_unsupported_fmon_cb (GHashTable *table, SubMonitor *smon) { if (g_hash_table_size(table) == 0) gam_kqueue_poller_remove_sub_monitor(&unsupported_fmon_poller, smon); } static void gam_kqueue_sub_monitor_set_missing (SubMonitor *smon) { Monitor *mon = MONITOR(smon); if (mon->fd >= 0) { close(mon->fd); mon->fd = -1; open_files--; } /* * A removed directory will normally not contain files, but we must * not assume it (we might receive events out of order, etc). We * therefore check if files are remaining, and if yes, clear them. */ if (g_hash_table_size(smon->fmons->main) > 0) { gam_kqueue_sub_monitor_clear_fmons(smon); gam_kqueue_sub_monitor_init_fmons(smon); } gam_kqueue_poller_remove_sub_monitor(&unsupported_smon_poller, smon); gam_kqueue_poller_add_sub_monitor(&missing_smon_poller, smon); } static void gam_kqueue_sub_monitor_set_unsupported (SubMonitor *smon) { Monitor *mon = MONITOR(smon); gam_kqueue_monitor_set_unsupported(mon); gam_kqueue_poller_add_sub_monitor(&unsupported_smon_poller, smon); } static void gam_kqueue_sub_monitor_enable_notification (SubMonitor *smon, SubMonitorFlags flags) { Monitor *mon = MONITOR(smon); gboolean exists; /* we first send CREATED or EXISTS/DELETED+ENDEXISTS events */ if ((flags & SUB_MONITOR_WAS_MISSING) != 0) exists = TRUE; else { struct stat sb; exists = lstat(mon->pathname, &sb) >= 0; flags |= (exists && (sb.st_mode & S_IFDIR) != 0) ? MONITOR_ISDIR : MONITOR_ISNOTDIR; } if (exists) { GaminEventType gevent; gevent = (flags & SUB_MONITOR_WAS_MISSING) != 0 ? GAMIN_EVENT_CREATED : GAMIN_EVENT_EXISTS; gam_kqueue_sub_monitor_emit_event(smon, gevent, flags); if (smon->isdir && (flags & MONITOR_ISDIR) != 0) { GDir *dir; GError *err = NULL; dir = g_dir_open(mon->pathname, 0, &err); if (dir) { const char *filename; while ((filename = g_dir_read_name(dir))) { FileMonitor *fmon; FileMonitorFlags fmon_flags; fmon = gam_kqueue_file_monitor_new(smon, filename, &fmon_flags); gam_kqueue_file_monitor_emit_event(fmon, gevent, fmon_flags); } g_dir_close(dir); } else { GAM_DEBUG(DEBUG_INFO, "unable to open directory %s: %s\n", mon->pathname, err->message); g_error_free(err); } } if ((flags & SUB_MONITOR_WAS_MISSING) == 0) gam_kqueue_sub_monitor_emit_event(smon, GAMIN_EVENT_ENDEXISTS, flags); } else { gam_kqueue_sub_monitor_emit_event(smon, GAMIN_EVENT_DELETED, flags); gam_kqueue_sub_monitor_emit_event(smon, GAMIN_EVENT_ENDEXISTS, flags); return; } /* then we enable kqueue notification, falling back to poll if necessary */ if (! gam_kqueue_monitor_enable_kqueue(mon)) gam_kqueue_sub_monitor_set_unsupported(smon); } static void gam_kqueue_sub_monitor_handle_directory_change_removal_cb (const char *filename, FileMonitor *fmon, GHashTable *filenames) { if (! g_hash_table_lookup(filenames, filename)) gam_kqueue_file_monitor_emit_event(fmon, GAMIN_EVENT_DELETED, MONITOR_ISNOTDIR); } static void gam_kqueue_sub_monitor_handle_directory_change (SubMonitor *smon, gboolean isdir) { Monitor *mon = MONITOR(smon); GHashTable *filenames; filenames = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); if (isdir) /* do not follow symbolic links */ { GDir *dir; GError *err = NULL; dir = g_dir_open(mon->pathname, 0, &err); if (dir) { const char *filename; while ((filename = g_dir_read_name(dir))) { g_hash_table_insert(filenames, g_strdup(filename), GINT_TO_POINTER(TRUE)); /* handle file creation */ if (! g_hash_table_lookup(smon->fmons->main, filename)) { FileMonitor *fmon; FileMonitorFlags fmon_flags; fmon = gam_kqueue_file_monitor_new(smon, filename, &fmon_flags); gam_kqueue_file_monitor_emit_event(fmon, GAMIN_EVENT_CREATED, fmon_flags); } } g_dir_close(dir); } else { GAM_DEBUG(DEBUG_INFO, "unable to open directory %s: %s\n", mon->pathname, err->message); g_error_free(err); } } /* * Handle deleted files (they are also handled at the FileMonitor * level, but we must use whichever event comes first). */ gam_kqueue_hash_table_foreach(smon->fmons, (GHFunc) gam_kqueue_sub_monitor_handle_directory_change_removal_cb, filenames); g_hash_table_destroy(filenames); } static void gam_kqueue_sub_monitor_emit_event (SubMonitor *smon, GaminEventType event, SubMonitorFlags flags) { Monitor *mon = MONITOR(smon); gboolean isdir; isdir = gam_kqueue_isdir(mon->pathname, flags); switch (event) { case GAMIN_EVENT_CHANGED: if (smon->isdir) { gam_kqueue_sub_monitor_handle_directory_change(smon, isdir); return; } break; case GAMIN_EVENT_DELETED: case GAMIN_EVENT_MOVED: gam_kqueue_sub_monitor_set_missing(smon); break; default: break; } gam_server_emit_event(mon->pathname, isdir, event, smon->subs, 1); } static void gam_kqueue_sub_monitor_handle_kevent (Monitor *mon, struct kevent *event) { SubMonitor *smon = SUB_MONITOR(mon); if ((event->flags & EV_ERROR) != 0) { /* kqueue failed, fallback to poll */ GAM_DEBUG(DEBUG_INFO, "kqueue failed for %s, falling back to poll\n", mon->pathname); gam_kqueue_sub_monitor_set_unsupported(smon); return; } /* * kevent aggregates events, so we must handle a fflags with * multiple event bits set. */ if ((event->fflags & VN_NOTE_CHANGED) != 0) gam_kqueue_sub_monitor_emit_event(smon, GAMIN_EVENT_CHANGED, 0); /* emitting the following events will add the smon to the missing poller */ if ((event->fflags & VN_NOTE_DELETED) != 0) gam_kqueue_sub_monitor_emit_event(smon, GAMIN_EVENT_DELETED, MONITOR_ISNOTDIR); else if ((event->fflags & NOTE_RENAME) != 0) gam_kqueue_sub_monitor_emit_event(smon, GAMIN_EVENT_MOVED, MONITOR_ISNOTDIR); } /*** FileMonitor *************************************************************/ static void gam_kqueue_file_monitor_set_unsupported (FileMonitor *fmon) { Monitor *mon = MONITOR(fmon); gam_kqueue_monitor_set_unsupported(mon); gam_kqueue_hash_table_add(fmon->smon->unsupported_fmons, fmon); } static FileMonitor * gam_kqueue_file_monitor_new (SubMonitor *smon, const char *filename, FileMonitorFlags *flags) { FileMonitor *fmon; Monitor *mon; fmon = g_new0(FileMonitor, 1); mon = MONITOR(fmon); mon->handle_kevent = gam_kqueue_file_monitor_handle_kevent; mon->pathname = g_build_filename(MONITOR(smon)->pathname, filename, NULL); mon->fd = -1; fmon->smon = smon; fmon->filename = strrchr(mon->pathname, G_DIR_SEPARATOR); fmon->filename = fmon->filename ? fmon->filename + 1 : mon->pathname; gam_kqueue_hash_table_add(fmon->smon->fmons, fmon); if (gam_kqueue_monitor_enable_kqueue(mon)) *flags = 0; else { gam_kqueue_file_monitor_set_unsupported(fmon); *flags = (mon->sb.mode & S_IFDIR) != 0 ? MONITOR_ISDIR : MONITOR_ISNOTDIR; } return fmon; } static void gam_kqueue_file_monitor_free (FileMonitor *fmon) { gam_kqueue_monitor_free(MONITOR(fmon)); } static void gam_kqueue_file_monitor_emit_event (FileMonitor *fmon, GaminEventType event, FileMonitorFlags flags) { Monitor *mon = MONITOR(fmon); struct stat sb; gboolean isdir; gboolean stat_done; gboolean stat_succeeded; if ((flags & MONITOR_ISDIR) == 0 && (flags & MONITOR_ISNOTDIR) == 0) { stat_done = TRUE; stat_succeeded = lstat(mon->pathname, &sb) >= 0; isdir = stat_succeeded && (sb.st_mode & S_IFDIR) != 0; } else { stat_done = FALSE; isdir = (flags & MONITOR_ISDIR) != 0; } gam_server_emit_event(mon->pathname, isdir, event, fmon->smon->subs, 1); switch (event) { case GAMIN_EVENT_DELETED: case GAMIN_EVENT_MOVED: if (mon->fd < 0) gam_kqueue_hash_table_remove(fmon->smon->unsupported_fmons, fmon); if ((flags & FILE_MONITOR_POSSIBLY_RECREATED) != 0) { if (! stat_done) stat_succeeded = lstat(mon->pathname, &sb) >= 0; if (stat_succeeded) { FileMonitor *new_fmon; FileMonitorFlags new_fmon_flags; /* * The file exists again. It means that kqueue has * aggregated a removal+creation into a single event. We * must therefore create a new fmon and emit a * GAMIN_EVENT_CREATED event, because * gam_kqueue_sub_monitor_handle_directory_change() did * not detect the removal+creation. */ new_fmon = gam_kqueue_file_monitor_new(fmon->smon, fmon->filename, &new_fmon_flags); gam_kqueue_file_monitor_emit_event(new_fmon, GAMIN_EVENT_CREATED, new_fmon_flags); break; /* do not remove the fmon we've just created */ } } gam_kqueue_hash_table_remove(fmon->smon->fmons, fmon); break; default: break; } } static void gam_kqueue_file_monitor_handle_kevent (Monitor *mon, struct kevent *event) { FileMonitor *fmon = FILE_MONITOR(mon); if ((event->flags & EV_ERROR) != 0) { /* kqueue failed, fallback to poll */ GAM_DEBUG(DEBUG_INFO, "kqueue failed for %s, falling back to poll\n", mon->pathname); gam_kqueue_file_monitor_set_unsupported(fmon); return; } /* * kevent aggregates events, so we must handle a fflags with * multiple event bits set. */ if ((event->fflags & VN_NOTE_CHANGED) != 0) gam_kqueue_file_monitor_emit_event(fmon, GAMIN_EVENT_CHANGED, 0); /* emitting the following events will free the fmon */ if ((event->fflags & VN_NOTE_DELETED) != 0) gam_kqueue_file_monitor_emit_event(fmon, GAMIN_EVENT_DELETED, MONITOR_ISNOTDIR | FILE_MONITOR_POSSIBLY_RECREATED); else if ((event->fflags & NOTE_RENAME) != 0) gam_kqueue_file_monitor_emit_event(fmon, GAMIN_EVENT_MOVED, MONITOR_ISNOTDIR | FILE_MONITOR_POSSIBLY_RECREATED); } /*** kevent/poll callbacks ***************************************************/ static gboolean gam_kqueue_kevent_cb (GIOChannel *source, GIOCondition condition, gpointer user_data) { int nevents; struct kevent ev[1]; struct timespec timeout = { 0, 0 }; int i; nevents = kevent(kq, NULL, 0, ev, G_N_ELEMENTS(ev), &timeout); if (nevents < 0) { GAM_DEBUG(DEBUG_INFO, "kevent() failure: %s\n", g_strerror(errno)); return TRUE; /* keep source */ } for (i = 0; i < nevents; i++) MONITOR(ev[i].udata)->handle_kevent(MONITOR(ev[i].udata), &ev[i]); return TRUE; /* keep source */ } static void gam_kqueue_missing_smon_poll (SubMonitor *smon) { struct stat sb; if (lstat(MONITOR(smon)->pathname, &sb) >= 0) { gam_kqueue_poller_remove_sub_monitor(&missing_smon_poller, smon); gam_kqueue_sub_monitor_enable_notification(smon, SUB_MONITOR_WAS_MISSING | ((sb.st_mode & S_IFDIR) != 0 ? MONITOR_ISDIR : MONITOR_ISNOTDIR)); } } static void gam_kqueue_unsupported_smon_poll (SubMonitor *smon) { Monitor *mon = MONITOR(smon); MiniStat sb; GaminEventType event; if (++mon->poll_count == CFG_UNSUPPORTED_SMON_KQUEUE_RETRY_FREQUENCY) { mon->poll_count = 0; if (gam_kqueue_monitor_enable_kqueue(mon)) gam_kqueue_poller_remove_sub_monitor(&missing_smon_poller, smon); } gam_kqueue_mini_lstat(mon->pathname, &sb); if (! sb.exists && mon->sb.exists) event = GAMIN_EVENT_DELETED; else if (gam_kqueue_differs(&sb, &mon->sb)) event = GAMIN_EVENT_CHANGED; else return; memcpy(&mon->sb, &sb, sizeof(sb)); gam_kqueue_sub_monitor_emit_event(smon, event, (sb.mode & S_IFDIR) != 0 ? MONITOR_ISDIR : MONITOR_ISNOTDIR); } static void gam_kqueue_unsupported_fmon_poll_foreach_cb (FileMonitor *fmon, gpointer unused, gpointer user_data) { Monitor *mon = MONITOR(fmon); MiniStat sb; GaminEventType event; if (++mon->poll_count == CFG_UNSUPPORTED_FMON_KQUEUE_RETRY_FREQUENCY) { mon->poll_count = 0; if (gam_kqueue_monitor_enable_kqueue(mon)) gam_kqueue_hash_table_remove(fmon->smon->unsupported_fmons, fmon); } gam_kqueue_mini_lstat(mon->pathname, &sb); if (! sb.exists && mon->sb.exists) event = GAMIN_EVENT_DELETED; else if (gam_kqueue_differs(&sb, &mon->sb)) event = GAMIN_EVENT_CHANGED; else return; memcpy(&mon->sb, &sb, sizeof(sb)); gam_kqueue_file_monitor_emit_event(fmon, event, (sb.mode & S_IFDIR) != 0 ? MONITOR_ISDIR : MONITOR_ISNOTDIR); } static void gam_kqueue_unsupported_fmon_poll (SubMonitor *smon) { gam_kqueue_hash_table_foreach(smon->unsupported_fmons, (GHFunc) gam_kqueue_unsupported_fmon_poll_foreach_cb, NULL); } /*** Gamin backend implementation ********************************************/ /** * Initializes the kqueue system. This must be called before * any other functions in this module. * * @returns TRUE if initialization succeeded, FALSE otherwise */ gboolean gam_kqueue_init (void) { GIOChannel *channel; unsigned int maxfiles; unsigned int maxfilesperproc; kq = kqueue(); if (kq < 0) { gam_error(DEBUG_INFO, "kqueue initialization failure: %s\n", g_strerror(errno)); return FALSE; } if (! gam_kqueue_get_uint_sysctl("kern.maxfiles", &maxfiles)) return FALSE; if (! gam_kqueue_get_uint_sysctl("kern.maxfilesperproc", &maxfilesperproc)) return FALSE; /* * We make sure to: * - never paralyze the system (CFG_GLOBAL_FILE_RESERVE_RATIO) * - never paralyze our own process (CFG_SELF_FILE_RESERVE) */ maxfiles *= CFG_GLOBAL_FILE_RESERVE_RATIO; maxfilesperproc = maxfilesperproc > CFG_SELF_FILE_RESERVE ? maxfilesperproc - CFG_SELF_FILE_RESERVE : 0; max_open_files = MIN(maxfiles, maxfilesperproc); dir_hash = g_hash_table_new(g_str_hash, g_str_equal); file_hash = g_hash_table_new(g_str_hash, g_str_equal); gam_kqueue_poller_init(&missing_smon_poller, gam_kqueue_missing_smon_poll, CFG_MISSING_SMON_POLL_INTERVAL); gam_kqueue_poller_init(&unsupported_smon_poller, gam_kqueue_unsupported_smon_poll, CFG_UNSUPPORTED_SMON_POLL_INTERVAL); gam_kqueue_poller_init(&unsupported_fmon_poller, gam_kqueue_unsupported_fmon_poll, CFG_UNSUPPORTED_FMON_POLL_INTERVAL); channel = g_io_channel_unix_new(kq); g_io_add_watch(channel, G_IO_IN, gam_kqueue_kevent_cb, NULL); #ifdef USE_GAMIN_POLLER gam_poll_basic_init (); #endif gam_server_install_kernel_hooks(GAMIN_K_KQUEUE, gam_kqueue_add_subscription, gam_kqueue_remove_subscription, gam_kqueue_remove_all_for, NULL, NULL); return TRUE; } /** * Adds a subscription to be monitored. * * @param sub a #GamSubscription to be polled * @returns TRUE if adding the subscription succeeded, FALSE otherwise */ gboolean gam_kqueue_add_subscription (GamSubscription *sub) { const char *path; GHashTable *hash; SubMonitor *smon; gam_listener_add_subscription(gam_subscription_get_listener(sub), sub); path = gam_subscription_get_path(sub); hash = gam_subscription_is_dir(sub) ? dir_hash : file_hash; smon = g_hash_table_lookup(hash, path); if (smon) { smon->subs = g_list_append(smon->subs, sub); return TRUE; } smon = gam_kqueue_sub_monitor_new(sub); smon->subs = g_list_append(smon->subs, sub); g_hash_table_insert(hash, MONITOR(smon)->pathname, smon); gam_kqueue_sub_monitor_enable_notification(smon, 0); return TRUE; } /** * Removes a subscription which was being monitored. * * @param sub a #GamSubscription to remove * @returns TRUE if removing the subscription succeeded, FALSE otherwise */ gboolean gam_kqueue_remove_subscription (GamSubscription *sub) { GHashTable *hash; SubMonitor *smon; hash = gam_subscription_is_dir(sub) ? dir_hash : file_hash; smon = g_hash_table_lookup(hash, gam_subscription_get_path(sub)); if (! smon) return FALSE; smon->subs = g_list_remove_all(smon->subs, sub); if (! smon->subs) { g_hash_table_remove(hash, MONITOR(smon)->pathname); gam_kqueue_sub_monitor_free(smon); } gam_subscription_cancel(sub); return TRUE; } /** * Stop monitoring all subscriptions for a given listener. * * @param listener a #GamListener * @returns TRUE if removing the subscriptions succeeded, FALSE otherwise */ gboolean gam_kqueue_remove_all_for (GamListener *listener) { GList *subs; GList *l; gboolean success = TRUE; subs = gam_listener_get_subscriptions(listener); for (l = subs; l != NULL; l = l->next) if (! gam_kqueue_remove_subscription(l->data)) success = FALSE; g_list_free(subs); return success; }