Merge branch 'wip/lantw/freebsd-kqueue-complex' into 'master'

FreeBSD kqueue file monitor fixes: the complex parts

See merge request GNOME/glib!77
This commit is contained in:
Philip Withnall 2018-06-19 11:19:43 +00:00
commit 40a84b3d1c
5 changed files with 546 additions and 176 deletions

View File

@ -33,6 +33,7 @@
#include <string.h>
#include <glib-object.h>
#include <glib/gfileutils.h>
#include <gio/gfilemonitor.h>
#include <gio/glocalfilemonitor.h>
#include <gio/giomodule.h>
@ -52,19 +53,44 @@ static int kq_queue = -1;
#define G_KQUEUE_FILE_MONITOR(inst) (G_TYPE_CHECK_INSTANCE_CAST ((inst), \
G_TYPE_KQUEUE_FILE_MONITOR, GKqueueFileMonitor))
/* C11 allows type redefinition, but GLib is configured to use C89, which causes
* clang to show warnings when we use a C11 feature. Since the C89 requirement
* is mostly used to support MSVC, we simply ignore the warning here because
* this file is never going to be useful on Windows. */
#ifdef __clang__
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wtypedef-redefinition"
#endif
typedef GLocalFileMonitorClass GKqueueFileMonitorClass;
typedef struct
/* When the file we are monitoring is a directory, sub_dir is subscribed to the
* directory itself and sub_file is NULL.
*
* When the file we are monitoring is a regular file, sub_dir is subscribed to
* the directory containing the file and sub_file is subscribed to the file
* being monitored. We have to monitor both because it is possible that the
* file chosen for monitoring doesn't exist when the file monitor is started.
* We monitor on its parent in order to get notification when it is created.
*
* To distinguish between a directory monitor and a regular file monitor, check
* whether sub_file is NULL. */
typedef struct _GKqueueFileMonitor
{
GLocalFileMonitor parent_instance;
kqueue_sub *sub;
kqueue_sub *sub_dir;
kqueue_sub *sub_file;
#ifndef O_EVTONLY
GFileMonitor *fallback;
GFile *fbfile;
#endif
} GKqueueFileMonitor;
#ifdef __clang__
#pragma clang diagnostic pop
#endif
GType g_kqueue_file_monitor_get_type (void);
G_DEFINE_TYPE_WITH_CODE (GKqueueFileMonitor, g_kqueue_file_monitor, G_TYPE_LOCAL_FILE_MONITOR,
g_io_extension_point_implement (G_LOCAL_FILE_MONITOR_EXTENSION_POINT_NAME,
@ -78,12 +104,23 @@ G_DEFINE_TYPE_WITH_CODE (GKqueueFileMonitor, g_kqueue_file_monitor, G_TYPE_LOCAL
#define O_KQFLAG O_EVTONLY
#endif
#define NOTE_ALL (NOTE_DELETE|NOTE_WRITE|NOTE_EXTEND|NOTE_ATTRIB|NOTE_RENAME)
static inline unsigned int
note_all (void)
{
unsigned int notes = NOTE_DELETE | NOTE_WRITE | NOTE_EXTEND | NOTE_ATTRIB | NOTE_RENAME;
#ifdef NOTE_TRUNCATE
notes |= NOTE_TRUNCATE;
#endif
#ifdef NOTE_CLOSE_WRITE
notes |= NOTE_CLOSE_WRITE;
#endif
return notes;
}
static gboolean g_kqueue_file_monitor_cancel (GFileMonitor* monitor);
static gboolean g_kqueue_file_monitor_is_supported (void);
static kqueue_sub *_kqsub_new (const gchar *, GLocalFileMonitor *, GFileMonitorSource *);
static kqueue_sub *_kqsub_new (gchar *, gchar *, GKqueueFileMonitor *, GFileMonitorSource *);
static void _kqsub_free (kqueue_sub *);
static gboolean _kqsub_cancel (kqueue_sub *);
@ -138,11 +175,18 @@ g_kqueue_file_monitor_finalize (GObject *object)
{
GKqueueFileMonitor *kqueue_monitor = G_KQUEUE_FILE_MONITOR (object);
if (kqueue_monitor->sub)
if (kqueue_monitor->sub_dir)
{
_kqsub_cancel (kqueue_monitor->sub);
_kqsub_free (kqueue_monitor->sub);
kqueue_monitor->sub = NULL;
_kqsub_cancel (kqueue_monitor->sub_dir);
_kqsub_free (kqueue_monitor->sub_dir);
kqueue_monitor->sub_dir = NULL;
}
if (kqueue_monitor->sub_file)
{
_kqsub_cancel (kqueue_monitor->sub_file);
_kqsub_free (kqueue_monitor->sub_file);
kqueue_monitor->sub_file = NULL;
}
#ifndef O_EVTONLY
@ -165,17 +209,51 @@ g_kqueue_file_monitor_start (GLocalFileMonitor *local_monitor,
GFileMonitorSource *source)
{
GKqueueFileMonitor *kqueue_monitor = G_KQUEUE_FILE_MONITOR (local_monitor);
kqueue_sub *sub;
const gchar *path;
kqueue_sub *sub_dir = NULL, *sub_file = NULL;
gchar *path_dir, *path_file, *file_basename;
path = filename;
if (path == NULL)
path = dirname;
/* There are three possible cases here:
*
* 1. Directory: dirname != NULL, basename == NULL, filename == NULL
* 2. Regular file: dirname != NULL, basename != NULL, filename == NULL
* 3. Hard links: dirname == NULL, basename == NULL, filename != NULL
*
* Note that we don't distinguish between case 2 and 3. Kqueue monitors
* files based on file descriptors, so we always receive events come from
* hard links.
*/
if (filename != NULL)
{
path_dir = g_path_get_dirname (filename);
path_file = g_strdup (filename);
file_basename = g_path_get_basename (filename);
}
else
{
path_dir = g_strdup (dirname);
if (basename != NULL)
{
path_file = g_build_filename (dirname, basename, NULL);
file_basename = g_strdup (basename);
}
else
{
path_file = NULL;
file_basename = NULL;
}
}
#ifndef O_EVTONLY
if (_ke_is_excluded (path))
if (_ke_is_excluded (path_dir))
{
GFile *file = g_file_new_for_path (path);
GFile *file;
if (path_file != NULL)
file = g_file_new_for_path (path_file);
else
file = g_file_new_for_path (path_dir);
g_free (path_dir);
g_free (path_file);
g_free (file_basename);
kqueue_monitor->fbfile = file;
kqueue_monitor->fallback = _g_poll_file_monitor_new (file);
g_signal_connect (kqueue_monitor->fallback, "changed",
@ -191,13 +269,30 @@ g_kqueue_file_monitor_start (GLocalFileMonitor *local_monitor,
* file, GIO uses a GKqueueFileMonitor object for that. If a directory
* will be created under that path, GKqueueFileMonitor will have to
* handle the directory notifications. */
sub = _kqsub_new (path, local_monitor, source);
if (sub == NULL)
return;
sub_dir = _kqsub_new (g_steal_pointer (&path_dir), NULL,
kqueue_monitor, source);
if (!_kqsub_start_watching (sub_dir))
_km_add_missing (sub_dir);
kqueue_monitor->sub = sub;
if (!_kqsub_start_watching (sub))
_km_add_missing (sub);
/* Unlike GInotifyFileMonitor, which always uses a directory monitor
* regardless of the type of the file being monitored, kqueue doesn't
* give us events generated by files under it when we are monitoring
* a directory. We have to monitor the file itself to know changes which
* was made to the file itself. */
if (path_file != NULL)
{
sub_file = _kqsub_new (g_steal_pointer (&path_file),
g_steal_pointer (&file_basename),
kqueue_monitor, source);
if (!_kqsub_start_watching (sub_file))
_km_add_missing (sub_file);
}
kqueue_monitor->sub_dir = sub_dir;
kqueue_monitor->sub_file = sub_file;
g_clear_pointer (&path_dir, g_free);
g_clear_pointer (&path_file, g_free);
g_clear_pointer (&file_basename, g_free);
}
static void
@ -230,58 +325,126 @@ g_kqueue_file_monitor_callback (gint fd, GIOCondition condition, gpointer user_d
struct timespec ts;
memset (&ts, 0, sizeof(ts));
/* We must hold the global lock before accessing any kqueue_sub because it is
* possible for other threads to call g_kqueue_file_monitor_cancel, which may
* free the kqueue_sub struct we are accessing. */
G_LOCK (kq_lock);
while (kevent(fd, NULL, 0, &ev, 1, &ts) > 0)
{
GFileMonitorEvent mask = 0;
if (ev.filter != EVFILT_VNODE || ev.udata == NULL)
continue;
sub = ev.udata;
source = sub->source;
/* When we are monitoring a regular file which already exists, ignore
* events generated by its parent directory. This has to be the first
* check to prevent the following code to emit useless events */
if (sub->is_dir && sub->mon->sub_file != NULL && sub->mon->sub_file->fd != -1)
continue;
if (ev.flags & EV_ERROR)
ev.fflags = NOTE_REVOKE;
if (ev.fflags & (NOTE_DELETE | NOTE_REVOKE))
if (sub->is_dir && ev.fflags & (NOTE_WRITE | NOTE_EXTEND))
{
/* If we are monitoring on a non-existent regular file, trigger the
* rescan of missing files immediately so we don't have to wait for
* 4 seconds for discovering missing files. We pass the sub_file
* corresponding to the GKqueueFileMonitor to 'check_this_sub_only'
* argument to prevent _km_scan_missing from emiting 'CREATED'
* events because _kh_dir_diff will do it for us. */
if (sub->mon->sub_file != NULL && sub->mon->sub_file->fd == -1)
_km_scan_missing (sub->mon->sub_file);
/* If we are monitoring a regular file, don't emit 'DELETED' events
* from the directory monitor because it will be emitted from the
* file itself when a NOTE_DELETE is reported on sub_file. */
_kh_dir_diff (sub, sub->mon->sub_file == NULL);
#ifdef NOTE_TRUNCATE
ev.fflags &= ~(NOTE_WRITE | NOTE_EXTEND | NOTE_TRUNCATE);
#else
ev.fflags &= ~(NOTE_WRITE | NOTE_EXTEND);
#endif
}
/* Here starts the long section of mapping kqueue events to
* GFileMonitorEvent. Since kqueue can return multiple events in a
* single kevent struct, we must use 'if' instead of 'else if'. */
if (ev.fflags & NOTE_DELETE)
{
struct stat st;
if (fstat (sub->fd, &st) < 0)
st.st_nlink = 0;
g_file_monitor_source_handle_event (source,
G_FILE_MONITOR_EVENT_DELETED,
sub->basename, NULL, NULL, now);
/* If the last reference to the file was removed, delete the
* subscription from kqueue and add it to the missing list.
* If you are monitoring a file which has hard link count higher
* than 1, it is possible for the same file to emit 'DELETED'
* events multiple times. */
if (st.st_nlink == 0)
{
_kqsub_cancel (sub);
_km_add_missing (sub);
}
if (sub->is_dir && ev.fflags & (NOTE_WRITE | NOTE_EXTEND))
{
_kh_dir_diff (sub);
ev.fflags &= ~(NOTE_WRITE | NOTE_EXTEND);
}
if (ev.fflags & NOTE_DELETE)
if (ev.fflags & NOTE_REVOKE)
{
mask = G_FILE_MONITOR_EVENT_DELETED;
g_file_monitor_source_handle_event (source,
G_FILE_MONITOR_EVENT_UNMOUNTED,
sub->basename, NULL, NULL, now);
_kqsub_cancel (sub);
_km_add_missing (sub);
}
else if (ev.fflags & NOTE_ATTRIB)
if (ev.fflags & NOTE_ATTRIB)
{
mask = G_FILE_MONITOR_EVENT_ATTRIBUTE_CHANGED;
g_file_monitor_source_handle_event (source,
G_FILE_MONITOR_EVENT_ATTRIBUTE_CHANGED,
sub->basename, NULL, NULL, now);
}
else if (ev.fflags & (NOTE_WRITE | NOTE_EXTEND))
#ifdef NOTE_TRUNCATE
if (ev.fflags & (NOTE_WRITE | NOTE_EXTEND | NOTE_TRUNCATE))
#else
if (ev.fflags & (NOTE_WRITE | NOTE_EXTEND))
#endif
{
mask = G_FILE_MONITOR_EVENT_CHANGED;
g_file_monitor_source_handle_event (source,
G_FILE_MONITOR_EVENT_CHANGED,
sub->basename, NULL, NULL, now);
}
else if (ev.fflags & NOTE_RENAME)
if (ev.fflags & NOTE_RENAME)
{
/* Since theres apparently no way to get the new name of the
* file out of kqueue(), all we can do is say that this one has
* been deleted. */
mask = G_FILE_MONITOR_EVENT_DELETED;
g_file_monitor_source_handle_event (source,
G_FILE_MONITOR_EVENT_DELETED,
sub->basename, NULL, NULL, now);
}
else if (ev.fflags & NOTE_REVOKE)
#ifdef NOTE_CLOSE_WRITE
if (ev.fflags & NOTE_CLOSE_WRITE)
{
mask = G_FILE_MONITOR_EVENT_UNMOUNTED;
g_file_monitor_source_handle_event (source,
G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT,
sub->basename, NULL, NULL, now);
}
#endif
/* Handle the case when a file is created again shortly after it was
* deleted. It has to be the last check because 'DELETED' must happen
* before 'CREATED'. */
if (ev.fflags & (NOTE_DELETE | NOTE_REVOKE))
_km_scan_missing (NULL);
}
if (mask)
g_file_monitor_source_handle_event (source, mask, NULL, NULL, NULL, now);
}
G_UNLOCK (kq_lock);
return TRUE;
}
@ -320,14 +483,28 @@ g_kqueue_file_monitor_cancel (GFileMonitor *monitor)
{
GKqueueFileMonitor *kqueue_monitor = G_KQUEUE_FILE_MONITOR (monitor);
if (kqueue_monitor->sub)
/* We must hold the global lock before calling _kqsub_cancel. However, we
* cannot call G_LOCK in _kqsub_cancel because it is also used by
* g_kqueue_file_monitor_callback, which already holds the lock itself. */
G_LOCK (kq_lock);
if (kqueue_monitor->sub_dir)
{
_kqsub_cancel (kqueue_monitor->sub);
_kqsub_free (kqueue_monitor->sub);
kqueue_monitor->sub = NULL;
_kqsub_cancel (kqueue_monitor->sub_dir);
_kqsub_free (kqueue_monitor->sub_dir);
kqueue_monitor->sub_dir = NULL;
}
if (kqueue_monitor->sub_file)
{
_kqsub_cancel (kqueue_monitor->sub_file);
_kqsub_free (kqueue_monitor->sub_file);
kqueue_monitor->sub_file = NULL;
}
G_UNLOCK (kq_lock);
#ifndef O_EVTONLY
else if (kqueue_monitor->fallback)
if (kqueue_monitor->fallback)
{
g_signal_handlers_disconnect_by_func (kqueue_monitor->fallback, _fallback_callback, kqueue_monitor);
g_file_monitor_cancel (kqueue_monitor->fallback);
@ -341,12 +518,13 @@ g_kqueue_file_monitor_cancel (GFileMonitor *monitor)
}
static kqueue_sub *
_kqsub_new (const gchar *filename, GLocalFileMonitor *mon, GFileMonitorSource *source)
_kqsub_new (gchar *filename, gchar *basename, GKqueueFileMonitor *mon, GFileMonitorSource *source)
{
kqueue_sub *sub;
sub = g_slice_new (kqueue_sub);
sub->filename = g_strdup (filename);
sub->filename = filename;
sub->basename = basename;
sub->mon = mon;
g_source_ref ((GSource *) source);
sub->source = source;
@ -365,19 +543,23 @@ _kqsub_free (kqueue_sub *sub)
g_source_unref ((GSource *) sub->source);
g_free (sub->filename);
g_free (sub->basename);
g_slice_free (kqueue_sub, sub);
}
static gboolean
_kqsub_cancel (kqueue_sub *sub)
{
/* WARNING: Before calling this function, you must hold a lock on kq_lock
* or you will cause use-after-free in g_kqueue_file_monitor_callback. */
struct kevent ev;
/* Remove the event and close the file descriptor to automatically
* delete pending events. */
if (sub->fd != -1)
{
EV_SET (&ev, sub->fd, EVFILT_VNODE, EV_DELETE, NOTE_ALL, 0, sub);
EV_SET (&ev, sub->fd, EVFILT_VNODE, EV_DELETE, note_all (), 0, sub);
if (kevent (kq_queue, &ev, 1, NULL, 0, NULL) == -1)
{
g_warning ("Unable to remove event for %s: %s", sub->filename, g_strerror (errno));
@ -425,7 +607,7 @@ _kqsub_start_watching (kqueue_sub *sub)
sub->deps = dl_listing (sub->filename);
}
EV_SET (&ev, sub->fd, EVFILT_VNODE, EV_ADD | EV_CLEAR, NOTE_ALL, 0, sub);
EV_SET (&ev, sub->fd, EVFILT_VNODE, EV_ADD | EV_CLEAR, note_all (), 0, sub);
if (kevent (kq_queue, &ev, 1, NULL, 0, NULL) == -1)
{
g_warning ("Unable to add event for %s: %s", sub->filename, g_strerror (errno));

View File

@ -25,6 +25,7 @@
#include <sys/event.h>
#include <sys/time.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <gio/glocalfile.h>
#include <gio/glocalfilemonitor.h>
#include <gio/gfile.h>
@ -38,6 +39,7 @@
typedef struct {
kqueue_sub *sub;
GFileMonitorSource *source;
gboolean handle_deleted;
} handle_ctx;
/**
@ -53,6 +55,9 @@ static void
handle_created (void *udata, const char *path, ino_t inode)
{
handle_ctx *ctx = NULL;
gint64 now;
gchar *fullname;
struct stat st;
(void) inode;
ctx = (handle_ctx *) udata;
@ -60,8 +65,16 @@ handle_created (void *udata, const char *path, ino_t inode)
g_assert (ctx->sub != NULL);
g_assert (ctx->source != NULL);
now = g_get_monotonic_time ();
g_file_monitor_source_handle_event (ctx->source, G_FILE_MONITOR_EVENT_CREATED, path,
NULL, NULL, g_get_monotonic_time ());
NULL, NULL, now);
/* Copied from ih_event_callback to report 'CHANGES_DONE_HINT' earlier. */
fullname = g_build_filename (ctx->sub->filename, path, NULL);
if (stat (fullname, &st) != 0 || !S_ISREG (st.st_mode) || st.st_nlink != 1)
g_file_monitor_source_handle_event (ctx->source, G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT, path,
NULL, NULL, now);
g_free (fullname);
}
/**
@ -84,6 +97,9 @@ handle_deleted (void *udata, const char *path, ino_t inode)
g_assert (ctx->sub != NULL);
g_assert (ctx->source != NULL);
if (!ctx->handle_deleted)
return;
g_file_monitor_source_handle_event (ctx->source, G_FILE_MONITOR_EVENT_DELETED, path,
NULL, NULL, g_get_monotonic_time ());
}
@ -161,7 +177,7 @@ static const traverse_cbs cbs = {
void
_kh_dir_diff (kqueue_sub *sub)
_kh_dir_diff (kqueue_sub *sub, gboolean handle_deleted)
{
dep_list *was;
handle_ctx ctx;
@ -169,6 +185,7 @@ _kh_dir_diff (kqueue_sub *sub)
memset (&ctx, 0, sizeof (handle_ctx));
ctx.sub = sub;
ctx.source = sub->source;
ctx.handle_deleted = handle_deleted;
was = sub->deps;
sub->deps = dl_listing (sub->filename);

View File

@ -28,26 +28,33 @@
#include "dep-list.h"
typedef struct _GKqueueFileMonitor GKqueueFileMonitor;
/**
* kqueue_sub:
* @mon: a pointer to the GKqueueFileMonitor which holds this subscription
* @filename: a name of the file to monitor
* @fd: the associated file descriptor (used by kqueue)
*
* Represents a subscription on a file or directory.
* Represents a subscription on a file or directory. To check whether a
* subscription is active, check the fd field. If fd is not -1, it is an
* active subscription which can emit events from kqueue.
*/
typedef struct
{
GLocalFileMonitor *mon;
GKqueueFileMonitor *mon;
GFileMonitorSource *source;
gchar* filename;
gchar* basename;
int fd;
dep_list* deps;
int is_dir;
} kqueue_sub;
gboolean _kqsub_start_watching (kqueue_sub *sub);
void _kh_dir_diff (kqueue_sub *sub);
void _kh_dir_diff (kqueue_sub *sub, gboolean handle_deleted);
void _km_add_missing (kqueue_sub *sub);
gboolean _km_scan_missing (kqueue_sub *check_this_sub_only);
void _km_remove (kqueue_sub *sub);
#endif /* __KQUEUE_HELPER_H */

View File

@ -21,16 +21,13 @@
*******************************************************************************/
#include <glib.h>
#include "glib-private.h"
#include "kqueue-helper.h"
#define SCAN_MISSING_TIME 4 /* 1/4 Hz */
void _kh_file_appeared_cb (kqueue_sub *sub);
static gboolean km_scan_missing (gpointer user_data);
static gboolean km_debug_enabled = FALSE;
#define KM_W if (km_debug_enabled) g_warning
@ -40,6 +37,12 @@ G_LOCK_DEFINE_STATIC (missing_lock);
static volatile gboolean scan_missing_running = FALSE;
static gboolean
_km_scan_missing_cb (gpointer user_data)
{
return _km_scan_missing (NULL);
}
/**
* _km_add_missing:
* @sub: a #kqueue_sub
@ -63,8 +66,12 @@ _km_add_missing (kqueue_sub *sub)
if (!scan_missing_running)
{
GSource *source;
scan_missing_running = TRUE;
g_timeout_add_seconds (SCAN_MISSING_TIME, km_scan_missing, NULL);
source = g_timeout_source_new_seconds (SCAN_MISSING_TIME);
g_source_set_callback (source, _km_scan_missing_cb, NULL, NULL);
g_source_attach (source, GLIB_PRIVATE_CALL (g_get_worker_context) ());
g_source_unref (source);
}
}
@ -77,10 +84,10 @@ _km_add_missing (kqueue_sub *sub)
* Signals that a missing file has finally appeared in the filesystem.
* Emits %G_FILE_MONITOR_EVENT_CREATED.
**/
void
static void
_kh_file_appeared_cb (kqueue_sub *sub)
{
GFile *child;
gint64 now = g_get_monotonic_time ();
g_assert (sub != NULL);
g_assert (sub->filename);
@ -88,18 +95,14 @@ _kh_file_appeared_cb (kqueue_sub *sub)
if (!g_file_test (sub->filename, G_FILE_TEST_EXISTS))
return;
child = g_file_new_for_path (sub->filename);
g_file_monitor_emit_event (G_FILE_MONITOR (sub->mon),
child,
NULL,
G_FILE_MONITOR_EVENT_CREATED);
g_object_unref (child);
g_file_monitor_source_handle_event (sub->source, G_FILE_MONITOR_EVENT_CREATED,
sub->basename, NULL, NULL, now);
g_file_monitor_source_handle_event (sub->source, G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT,
sub->basename, NULL, NULL, now);
}
/**
* km_scan_missing:
* _km_scan_missing:
* @user_data: unused
*
* The core missing files watching routine.
@ -110,8 +113,8 @@ _kh_file_appeared_cb (kqueue_sub *sub)
*
* Returns: %FALSE if no missing files left, %TRUE otherwise.
**/
static gboolean
km_scan_missing (gpointer user_data)
gboolean
_km_scan_missing (kqueue_sub *check_this_sub_only)
{
GSList *head;
GSList *not_missing = NULL;
@ -128,9 +131,13 @@ km_scan_missing (gpointer user_data)
g_assert (sub != NULL);
g_assert (sub->filename != NULL);
if (check_this_sub_only != NULL && sub != check_this_sub_only)
continue;
if (_kqsub_start_watching (sub))
{
KM_W ("file %s now exists, starting watching", sub->filename);
if (check_this_sub_only == NULL)
_kh_file_appeared_cb (sub);
not_missing = g_slist_prepend (not_missing, head);
}

View File

@ -9,12 +9,25 @@
* the tests, e.g. the length of timeouts
*/
typedef enum {
NONE = 0,
INOTIFY = (1 << 1),
KQUEUE = (1 << 2)
} Environment;
typedef struct
{
gint event_type;
gchar *file;
gchar *other_file;
gint step;
/* Since different file monitor implementation has different capabilities,
* we cannot expect all implementations to report all kind of events without
* any loss. This 'optional' field is a bit mask used to mark events which
* may be lost under specific platforms.
*/
Environment optional;
} RecordedEvent;
static void
@ -68,15 +81,139 @@ output_events (GList *list)
/* a placeholder for temp file names we don't want to compare */
static const gchar DONT_CARE[] = "";
static Environment
get_environment (GFileMonitor *monitor)
{
if (g_str_equal (G_OBJECT_TYPE_NAME (monitor), "GInotifyFileMonitor"))
return INOTIFY;
if (g_str_equal (G_OBJECT_TYPE_NAME (monitor), "GKqueueFileMonitor"))
return KQUEUE;
return NONE;
}
static void
check_expected_event (gint i,
RecordedEvent *e1,
RecordedEvent *e2)
check_expected_events (RecordedEvent *expected,
gsize n_expected,
GList *recorded,
Environment env)
{
gint i, li;
GList *l;
for (i = 0, li = 0, l = recorded; i < n_expected && l != NULL;)
{
RecordedEvent *e1 = &expected[i];
RecordedEvent *e2 = l->data;
gboolean mismatch = TRUE;
gboolean l_extra_step = FALSE;
do
{
gboolean ignore_other_file = FALSE;
if (e1->step != e2->step)
break;
/* Kqueue isn't good at detecting file renaming, so
* G_FILE_MONITOR_WATCH_MOVES is mostly useless there. */
if (e1->event_type != e2->event_type && env & KQUEUE)
{
/* It is possible for kqueue file monitor to emit 'RENAMED' event,
* but most of the time it is reported as a 'DELETED' event and
* a 'CREATED' event. */
if (e1->event_type == G_FILE_MONITOR_EVENT_RENAMED)
{
RecordedEvent *e2_next;
if (l->next == NULL)
break;
e2_next = l->next->data;
if (e2->event_type != G_FILE_MONITOR_EVENT_DELETED)
break;
if (e2_next->event_type != G_FILE_MONITOR_EVENT_CREATED)
break;
if (e1->step != e2_next->step)
break;
if (e1->file != DONT_CARE &&
(g_strcmp0 (e1->file, e2->file) != 0 ||
e2->other_file != NULL))
break;
if (e1->other_file != DONT_CARE &&
(g_strcmp0 (e1->other_file, e2_next->file) != 0 ||
e2_next->other_file != NULL))
break;
l_extra_step = TRUE;
mismatch = FALSE;
break;
}
/* Kqueue won't report 'MOVED_IN' and 'MOVED_OUT' events. We set
* 'ignore_other_file' here to let the following code know that
* 'other_file' may not match. */
else if (e1->event_type == G_FILE_MONITOR_EVENT_MOVED_IN)
{
if (e2->event_type != G_FILE_MONITOR_EVENT_CREATED)
break;
ignore_other_file = TRUE;
}
else if (e1->event_type == G_FILE_MONITOR_EVENT_MOVED_OUT)
{
if (e2->event_type != G_FILE_MONITOR_EVENT_DELETED)
break;
ignore_other_file = TRUE;
}
else
break;
}
if (e1->file != DONT_CARE &&
g_strcmp0 (e1->file, e2->file) != 0)
break;
if (e1->other_file != DONT_CARE && !ignore_other_file &&
g_strcmp0 (e1->other_file, e2->other_file) != 0)
break;
mismatch = FALSE;
}
while (0);
if (mismatch)
{
/* Sometimes the emission of 'CHANGES_DONE_HINT' may be late because
* it depends on the ability of file monitor implementation to report
* 'CHANGES_DONE_HINT' itself. If the file monitor implementation
* doesn't report 'CHANGES_DONE_HINT' itself, it may be emitted by
* GLocalFileMonitor after a few seconds, which causes the event to
* mix with results from different steps. Since 'CHANGES_DONE_HINT'
* is just a hint, we don't require it to be reliable and we simply
* ignore unexpected 'CHANGES_DONE_HINT' events here. */
if (e1->event_type != G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT &&
e2->event_type == G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT)
{
g_test_message ("Event CHANGES_DONE_HINT ignored at "
"expected index %d, recorded index %d", i, li);
li++, l = l->next;
continue;
}
/* If an event is marked as optional in the current environment and
* the event doesn't match, it means the expected event has lost. */
else if (env & e1->optional)
{
g_test_message ("Event %d at expected index %d skipped because "
"it is marked as optional", e1->event_type, i);
i++;
continue;
}
/* Run above checks under g_assert_* again to provide more useful
* error messages. */
else
{
g_assert_cmpint (e1->step, ==, e2->step);
if (e1->step < 0)
return;
g_assert_cmpint (e1->event_type, ==, e2->event_type);
if (e1->file != DONT_CARE)
@ -84,25 +221,18 @@ check_expected_event (gint i,
if (e1->other_file != DONT_CARE)
g_assert_cmpstr (e1->other_file, ==, e2->other_file);
g_assert_not_reached ();
}
}
static void
check_expected_events (RecordedEvent *expected,
gsize n_expected,
GList *recorded)
{
gint i;
GList *l;
g_assert_cmpint (n_expected, ==, g_list_length (recorded));
for (i = 0, l = recorded; i < n_expected; i++, l = l->next)
{
RecordedEvent *e1 = &expected[i];
RecordedEvent *e2 = (RecordedEvent *)l->data;
check_expected_event (i, e1, e2);
i++, li++, l = l->next;
if (l_extra_step)
li++, l = l->next;
}
g_assert_cmpint (i, ==, n_expected);
g_assert_cmpint (li, ==, g_list_length (recorded));
}
static void
@ -180,15 +310,15 @@ atomic_replace_step (gpointer user_data)
/* this is the output we expect from the above steps */
static RecordedEvent atomic_replace_output[] = {
{ -1, NULL, NULL, 0 },
{ G_FILE_MONITOR_EVENT_CREATED, "atomic_replace_file", NULL, -1 },
{ G_FILE_MONITOR_EVENT_CHANGED, "atomic_replace_file", NULL, -1 },
{ G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT, "atomic_replace_file", NULL, -1 },
{ -1, NULL, NULL, 1 },
{ G_FILE_MONITOR_EVENT_RENAMED, (gchar*)DONT_CARE, "atomic_replace_file", -1 },
{ -1, NULL, NULL, 2 },
{ G_FILE_MONITOR_EVENT_DELETED, "atomic_replace_file", NULL, -1 },
{ -1, NULL, NULL, 3 }
{ -1, NULL, NULL, 0, NONE },
{ G_FILE_MONITOR_EVENT_CREATED, "atomic_replace_file", NULL, -1, NONE },
{ G_FILE_MONITOR_EVENT_CHANGED, "atomic_replace_file", NULL, -1, KQUEUE },
{ G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT, "atomic_replace_file", NULL, -1, KQUEUE },
{ -1, NULL, NULL, 1, NONE },
{ G_FILE_MONITOR_EVENT_RENAMED, (gchar*)DONT_CARE, "atomic_replace_file", -1, NONE },
{ -1, NULL, NULL, 2, NONE },
{ G_FILE_MONITOR_EVENT_DELETED, "atomic_replace_file", NULL, -1, NONE },
{ -1, NULL, NULL, 3, NONE }
};
static void
@ -216,7 +346,10 @@ test_atomic_replace (void)
g_main_loop_run (data.loop);
/*output_events (data.events);*/
check_expected_events (atomic_replace_output, G_N_ELEMENTS (atomic_replace_output), data.events);
check_expected_events (atomic_replace_output,
G_N_ELEMENTS (atomic_replace_output),
data.events,
get_environment (data.monitor));
g_list_free_full (data.events, (GDestroyNotify)free_recorded_event);
g_main_loop_unref (data.loop);
@ -277,18 +410,18 @@ change_step (gpointer user_data)
/* this is the output we expect from the above steps */
static RecordedEvent change_output[] = {
{ -1, NULL, NULL, 0 },
{ G_FILE_MONITOR_EVENT_CREATED, "change_file", NULL, -1 },
{ G_FILE_MONITOR_EVENT_CHANGED, "change_file", NULL, -1 },
{ G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT, "change_file", NULL, -1 },
{ -1, NULL, NULL, 1 },
{ G_FILE_MONITOR_EVENT_CHANGED, "change_file", NULL, -1 },
{ G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT, "change_file", NULL, -1 },
{ -1, NULL, NULL, 2 },
{ G_FILE_MONITOR_EVENT_ATTRIBUTE_CHANGED, "change_file", NULL, -1 },
{ -1, NULL, NULL, 3 },
{ G_FILE_MONITOR_EVENT_DELETED, "change_file", NULL, -1 },
{ -1, NULL, NULL, 4 }
{ -1, NULL, NULL, 0, NONE },
{ G_FILE_MONITOR_EVENT_CREATED, "change_file", NULL, -1, NONE },
{ G_FILE_MONITOR_EVENT_CHANGED, "change_file", NULL, -1, KQUEUE },
{ G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT, "change_file", NULL, -1, KQUEUE },
{ -1, NULL, NULL, 1, NONE },
{ G_FILE_MONITOR_EVENT_CHANGED, "change_file", NULL, -1, NONE },
{ G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT, "change_file", NULL, -1, NONE },
{ -1, NULL, NULL, 2, NONE },
{ G_FILE_MONITOR_EVENT_ATTRIBUTE_CHANGED, "change_file", NULL, -1, NONE },
{ -1, NULL, NULL, 3, NONE },
{ G_FILE_MONITOR_EVENT_DELETED, "change_file", NULL, -1, NONE },
{ -1, NULL, NULL, 4, NONE }
};
static void
@ -316,7 +449,10 @@ test_file_changes (void)
g_main_loop_run (data.loop);
/*output_events (data.events);*/
check_expected_events (change_output, G_N_ELEMENTS (change_output), data.events);
check_expected_events (change_output,
G_N_ELEMENTS (change_output),
data.events,
get_environment (data.monitor));
g_list_free_full (data.events, (GDestroyNotify)free_recorded_event);
g_main_loop_unref (data.loop);
@ -391,16 +527,16 @@ dir_step (gpointer user_data)
/* this is the output we expect from the above steps */
static RecordedEvent dir_output[] = {
{ -1, NULL, NULL, 1 },
{ -1, NULL, NULL, 2 },
{ G_FILE_MONITOR_EVENT_MOVED_IN, "dir_test_file", NULL, -1 },
{ -1, NULL, NULL, 3 },
{ G_FILE_MONITOR_EVENT_RENAMED, "dir_test_file", "dir_test_file2", -1 },
{ -1, NULL, NULL, 4 },
{ G_FILE_MONITOR_EVENT_MOVED_OUT, "dir_test_file2", NULL, -1 },
{ -1, NULL, NULL, 5 },
{ G_FILE_MONITOR_EVENT_DELETED, "dir_monitor_test", NULL, -1 },
{ -1, NULL, NULL, 6 }
{ -1, NULL, NULL, 1, NONE },
{ -1, NULL, NULL, 2, NONE },
{ G_FILE_MONITOR_EVENT_MOVED_IN, "dir_test_file", NULL, -1, NONE },
{ -1, NULL, NULL, 3, NONE },
{ G_FILE_MONITOR_EVENT_RENAMED, "dir_test_file", "dir_test_file2", -1, NONE },
{ -1, NULL, NULL, 4, NONE },
{ G_FILE_MONITOR_EVENT_MOVED_OUT, "dir_test_file2", NULL, -1, NONE },
{ -1, NULL, NULL, 5, NONE },
{ G_FILE_MONITOR_EVENT_DELETED, "dir_monitor_test", NULL, -1, NONE },
{ -1, NULL, NULL, 6, NONE }
};
static void
@ -429,7 +565,10 @@ test_dir_monitor (void)
g_main_loop_run (data.loop);
/*output_events (data.events);*/
check_expected_events (dir_output, G_N_ELEMENTS (dir_output), data.events);
check_expected_events (dir_output,
G_N_ELEMENTS (dir_output),
data.events,
get_environment (data.monitor));
g_list_free_full (data.events, (GDestroyNotify)free_recorded_event);
g_main_loop_unref (data.loop);
@ -482,17 +621,17 @@ nodir_step (gpointer user_data)
}
static RecordedEvent nodir_output[] = {
{ -1, NULL, NULL, 0 },
{ G_FILE_MONITOR_EVENT_CREATED, "nosuchfile", NULL, -1 },
{ G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT, "nosuchfile", NULL, -1 },
{ -1, NULL, NULL, 1 },
{ G_FILE_MONITOR_EVENT_CREATED, "nosuchfile", NULL, -1 },
{ G_FILE_MONITOR_EVENT_CHANGED, "nosuchfile", NULL, -1 },
{ G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT, "nosuchfile", NULL, -1 },
{ -1, NULL, NULL, 2 },
{ G_FILE_MONITOR_EVENT_DELETED, "nosuchfile", NULL, -1 },
{ -1, NULL, NULL, 3 },
{ -1, NULL, NULL, 4 }
{ -1, NULL, NULL, 0, NONE },
{ G_FILE_MONITOR_EVENT_CREATED, "nosuchfile", NULL, -1, KQUEUE },
{ G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT, "nosuchfile", NULL, -1, KQUEUE },
{ -1, NULL, NULL, 1, NONE },
{ G_FILE_MONITOR_EVENT_CREATED, "nosuchfile", NULL, -1, NONE },
{ G_FILE_MONITOR_EVENT_CHANGED, "nosuchfile", NULL, -1, KQUEUE },
{ G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT, "nosuchfile", NULL, -1, KQUEUE },
{ -1, NULL, NULL, 2, NONE },
{ G_FILE_MONITOR_EVENT_DELETED, "nosuchfile", NULL, -1, NONE },
{ -1, NULL, NULL, 3, NONE },
{ -1, NULL, NULL, 4, NONE }
};
static void
@ -521,7 +660,10 @@ test_dir_non_existent (void)
g_main_loop_run (data.loop);
/*output_events (data.events);*/
check_expected_events (nodir_output, G_N_ELEMENTS (nodir_output), data.events);
check_expected_events (nodir_output,
G_N_ELEMENTS (nodir_output),
data.events,
get_environment (data.monitor));
g_list_free_full (data.events, (GDestroyNotify)free_recorded_event);
g_main_loop_unref (data.loop);
@ -578,26 +720,26 @@ cross_dir_step (gpointer user_data)
}
static RecordedEvent cross_dir_a_output[] = {
{ -1, NULL, NULL, 0 },
{ -1, NULL, NULL, 1 },
{ G_FILE_MONITOR_EVENT_CREATED, "a", NULL, -1 },
{ G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT, "a", NULL, -1 },
{ -1, NULL, NULL, 2 },
{ G_FILE_MONITOR_EVENT_DELETED, "a", NULL, -1 },
{ G_FILE_MONITOR_EVENT_DELETED, "cross_dir_a", NULL, -1 },
{ -1, NULL, NULL, 3 },
{ -1, NULL, NULL, 0, NONE },
{ -1, NULL, NULL, 1, NONE },
{ G_FILE_MONITOR_EVENT_CREATED, "a", NULL, -1, NONE },
{ G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT, "a", NULL, -1, KQUEUE },
{ -1, NULL, NULL, 2, NONE },
{ G_FILE_MONITOR_EVENT_DELETED, "a", NULL, -1, NONE },
{ G_FILE_MONITOR_EVENT_DELETED, "cross_dir_a", NULL, -1, NONE },
{ -1, NULL, NULL, 3, NONE },
};
static RecordedEvent cross_dir_b_output[] = {
{ -1, NULL, NULL, 0 },
{ G_FILE_MONITOR_EVENT_CREATED, "a", NULL, -1 },
{ G_FILE_MONITOR_EVENT_CHANGED, "a", NULL, -1 },
{ G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT, "a", NULL, -1 },
{ -1, NULL, NULL, 1 },
{ G_FILE_MONITOR_EVENT_MOVED_OUT, "a", "a", -1 },
{ -1, NULL, NULL, 2 },
{ G_FILE_MONITOR_EVENT_DELETED, "cross_dir_b", NULL, -1 },
{ -1, NULL, NULL, 3 },
{ -1, NULL, NULL, 0, NONE },
{ G_FILE_MONITOR_EVENT_CREATED, "a", NULL, -1, NONE },
{ G_FILE_MONITOR_EVENT_CHANGED, "a", NULL, -1, KQUEUE },
{ G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT, "a", NULL, -1, KQUEUE },
{ -1, NULL, NULL, 1, NONE },
{ G_FILE_MONITOR_EVENT_MOVED_OUT, "a", "a", -1, NONE },
{ -1, NULL, NULL, 2, NONE },
{ G_FILE_MONITOR_EVENT_DELETED, "cross_dir_b", NULL, -1, NONE },
{ -1, NULL, NULL, 3, NONE },
};
static void
test_cross_dir_moves (void)
@ -644,8 +786,14 @@ test_cross_dir_moves (void)
output_events (data[1].events);
#endif
check_expected_events (cross_dir_a_output, G_N_ELEMENTS (cross_dir_a_output), data[0].events);
check_expected_events (cross_dir_b_output, G_N_ELEMENTS (cross_dir_b_output), data[1].events);
check_expected_events (cross_dir_a_output,
G_N_ELEMENTS (cross_dir_a_output),
data[0].events,
get_environment (data[0].monitor));
check_expected_events (cross_dir_b_output,
G_N_ELEMENTS (cross_dir_b_output),
data[1].events,
get_environment (data[1].monitor));
g_list_free_full (data[0].events, (GDestroyNotify)free_recorded_event);
g_main_loop_unref (data[0].loop);
@ -742,19 +890,26 @@ file_hard_links_step (gpointer user_data)
}
static RecordedEvent file_hard_links_output[] = {
{ -1, NULL, NULL, 0 },
{ G_FILE_MONITOR_EVENT_CHANGED, "testfilemonitor.db", NULL, -1 },
{ G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT, "testfilemonitor.db", NULL, -1 },
{ -1, NULL, NULL, 1 },
{ G_FILE_MONITOR_EVENT_RENAMED, NULL /* .goutputstream-XXXXXX */, "testfilemonitor.db", -1 },
{ -1, NULL, NULL, 2 },
{ -1, NULL, NULL, 3 },
/* FIXME: There should be a EVENT_CHANGED and EVENT_CHANGES_DONE_HINT here
* from the modification of the hard link. */
{ -1, NULL, NULL, 4 },
{ G_FILE_MONITOR_EVENT_DELETED, "testfilemonitor.db", NULL, -1 },
{ -1, NULL, NULL, 5 },
{ -1, NULL, NULL, 6 },
{ -1, NULL, NULL, 0, NONE },
{ G_FILE_MONITOR_EVENT_CHANGED, "testfilemonitor.db", NULL, -1, NONE },
{ G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT, "testfilemonitor.db", NULL, -1, NONE },
{ -1, NULL, NULL, 1, NONE },
{ G_FILE_MONITOR_EVENT_RENAMED, (gchar*)DONT_CARE /* .goutputstream-XXXXXX */, "testfilemonitor.db", -1, NONE },
{ -1, NULL, NULL, 2, NONE },
{ -1, NULL, NULL, 3, NONE },
/* Kqueue is based on file descriptors. You can get events from all hard
* links by just monitoring one open file descriptor, and it is not possible
* to know whether it is done on the file name we use to open the file. Since
* the hard link count of 'testfilemonitor.db' is 2, it is expected to see
* two 'DELETED' events reported here. You have to call 'unlink' twice on
* different file names to remove 'testfilemonitor.db' from the file system,
* and each 'unlink' call generates a 'DELETED' event. */
{ G_FILE_MONITOR_EVENT_CHANGED, "testfilemonitor.db", NULL, -1, INOTIFY },
{ -1, NULL, NULL, 4, NONE },
{ G_FILE_MONITOR_EVENT_DELETED, "testfilemonitor.db", NULL, -1, NONE },
{ -1, NULL, NULL, 5, NONE },
{ G_FILE_MONITOR_EVENT_DELETED, "testfilemonitor.db", NULL, -1, INOTIFY },
{ -1, NULL, NULL, 6, NONE },
};
static void
@ -800,7 +955,9 @@ test_file_hard_links (void)
/* output_events (data.events); */
check_expected_events (file_hard_links_output,
G_N_ELEMENTS (file_hard_links_output), data.events);
G_N_ELEMENTS (file_hard_links_output),
data.events,
get_environment (data.monitor));
g_list_free_full (data.events, (GDestroyNotify) free_recorded_event);
g_main_loop_unref (data.loop);