/*
* 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 <marcus@FreeBSD.org>
* Copyright (C) 2005, 2006 Jean-Yves Lefort <jylefort@FreeBSD.org>
* Copyright (C) 2006 Alex Dupre <ale@FreeBSD.org>
*
* 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 <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/param.h>
#ifndef USE_GAMIN_POLLER
#include <sys/mount.h>
#endif
#include <sys/types.h>
#include <sys/sysctl.h>
#include <sys/stat.h>
#include <sys/event.h>
#include <sys/time.h>
#include <errno.h>
#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;
}
syntax highlighted by Code2HTML, v. 0.9.1