/* dzl-recursive-file-monitor.c * * Copyright (C) 2017 Christian Hergert * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #define G_LOG_DOMAIN "dzl-recursive-file-monitor" #include "config.h" #include #include #include "files/dzl-recursive-file-monitor.h" #include "util/dzl-macros.h" #define MONITOR_FLAGS 0 /** * SECTION:dzl-recursive-file-monitor * @title: DzlRecursiveFileMonitor * @short_description: a recursive directory monitor * * This works by creating a #GFileMonitor for each directory underneath a root * directory (and recursively beyond that). * * This is only designed for use on Linux, where we are using a single inotify * FD. You can still hit the max watch limit, but it is much higher than the FD * limit. * * Since: 3.28 */ struct _DzlRecursiveFileMonitor { GObject parent_instance; GFile *root; GCancellable *cancellable; GHashTable *monitors_by_file; GHashTable *files_by_monitor; DzlRecursiveIgnoreFunc ignore_func; gpointer ignore_func_data; GDestroyNotify ignore_func_data_destroy; }; enum { PROP_0, PROP_ROOT, N_PROPS }; enum { CHANGED, N_SIGNALS }; G_DEFINE_TYPE (DzlRecursiveFileMonitor, dzl_recursive_file_monitor, G_TYPE_OBJECT) static GParamSpec *properties [N_PROPS]; static guint signals [N_SIGNALS]; static void dzl_recursive_file_monitor_track (DzlRecursiveFileMonitor *self, GFile *dir, GFileMonitor *monitor); static void dzl_recursive_file_monitor_unwatch (DzlRecursiveFileMonitor *self, GFile *file) { GFileMonitor *monitor; dzl_assert_is_main_thread (); g_assert (DZL_IS_RECURSIVE_FILE_MONITOR (self)); g_assert (G_IS_FILE (file)); monitor = g_hash_table_lookup (self->monitors_by_file, file); if (monitor != NULL) { g_object_ref (monitor); g_file_monitor_cancel (monitor); g_hash_table_remove (self->monitors_by_file, file); g_hash_table_remove (self->files_by_monitor, monitor); g_object_unref (monitor); } } static void dzl_recursive_file_monitor_collect_recursive (GPtrArray *dirs, GFile *parent, GCancellable *cancellable) { g_autoptr(GFileEnumerator) enumerator = NULL; g_autoptr(GError) error = NULL; g_assert (dirs != NULL); g_assert (G_IS_FILE (parent)); g_assert (G_IS_CANCELLABLE (cancellable)); enumerator = g_file_enumerate_children (parent, G_FILE_ATTRIBUTE_STANDARD_NAME"," G_FILE_ATTRIBUTE_STANDARD_TYPE, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, cancellable, &error); if (error != NULL) { g_warning ("Failed to iterate children: %s", error->message); g_clear_error (&error); } if (enumerator != NULL) { gpointer infoptr; while (NULL != (infoptr = g_file_enumerator_next_file (enumerator, cancellable, NULL))) { g_autoptr(GFileInfo) info = infoptr; if (g_file_info_get_file_type (info) == G_FILE_TYPE_DIRECTORY) { const gchar *name = g_file_info_get_name (info); g_autoptr(GFile) child = g_file_get_child (parent, name); /* * We add the child, and then recurse into the child immediately * so that we can keep the invariant that all descendants * immediately follow their ancestor. This allows us to simplify * our ignored-directory checks when we get back to the main * thread. */ g_ptr_array_add (dirs, g_object_ref (child)); dzl_recursive_file_monitor_collect_recursive (dirs, child, cancellable); } } g_file_enumerator_close (enumerator, cancellable, NULL); g_clear_object (&enumerator); } } static GFile * resolve_file (GFile *file) { g_autofree gchar *orig_path = NULL; g_autoptr(GFile) new_file = NULL; char *real_path; g_assert (G_IS_FILE (file)); /* * The goal here is to work our way up to the root and resolve any * symlinks in the path. If the file is not native, we don't care * about symlinks. */ if (!g_file_is_native (file)) return g_object_ref (file); orig_path = g_file_get_path (file); real_path = realpath (orig_path, NULL); /* unlikely, but PATH_MAX exceeded */ if (real_path == NULL) return g_object_ref (file); new_file = g_file_new_for_path (real_path); free (real_path); return g_steal_pointer (&new_file); } static void dzl_recursive_file_monitor_collect_worker (GTask *task, gpointer source_object, gpointer task_data, GCancellable *cancellable) { g_autoptr(GPtrArray) dirs = NULL; g_autoptr(GFile) resolved = NULL; GFile *root = task_data; g_assert (G_IS_TASK (task)); g_assert (G_IS_FILE (root)); /* The first thing we want to do is resolve any symlinks out of * the path so that we are consistently working with the real * system path. This improves interaction with other APIs that * might not have given the callee back the symlink'd path and * instead the real path. */ resolved = resolve_file (root); dirs = g_ptr_array_new_with_free_func (g_object_unref); g_ptr_array_add (dirs, g_object_ref (resolved)); dzl_recursive_file_monitor_collect_recursive (dirs, resolved, cancellable); g_task_return_pointer (task, g_steal_pointer (&dirs), (GDestroyNotify)g_ptr_array_unref); } static void dzl_recursive_file_monitor_collect (DzlRecursiveFileMonitor *self, GFile *root, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data) { g_autoptr(GTask) task = NULL; g_assert (DZL_IS_RECURSIVE_FILE_MONITOR (self)); g_assert (G_IS_FILE (root)); g_assert (G_IS_CANCELLABLE (cancellable)); task = g_task_new (self, cancellable, callback, user_data); g_task_set_source_tag (task, dzl_recursive_file_monitor_collect); g_task_set_priority (task, G_PRIORITY_LOW); g_task_set_task_data (task, g_object_ref (root), g_object_unref); g_task_run_in_thread (task, dzl_recursive_file_monitor_collect_worker); } static GPtrArray * dzl_recursive_file_monitor_collect_finish (DzlRecursiveFileMonitor *self, GAsyncResult *result, GError **error) { g_assert (DZL_IS_RECURSIVE_FILE_MONITOR (self)); g_assert (G_IS_TASK (result)); g_assert (g_task_is_valid (G_TASK (result), self)); return g_task_propagate_pointer (G_TASK (result), error); } static gboolean dzl_recursive_file_monitor_ignored (DzlRecursiveFileMonitor *self, GFile *file) { dzl_assert_is_main_thread (); g_assert (DZL_IS_RECURSIVE_FILE_MONITOR (self)); g_assert (G_IS_FILE (file)); if (self->ignore_func != NULL) return self->ignore_func (file, self->ignore_func_data); return FALSE; } static void dzl_recursive_file_monitor_changed (DzlRecursiveFileMonitor *self, GFile *file, GFile *other_file, GFileMonitorEvent event, GFileMonitor *monitor) { dzl_assert_is_main_thread (); g_assert (DZL_IS_RECURSIVE_FILE_MONITOR (self)); g_assert (G_IS_FILE (file)); g_assert (!other_file || G_IS_FILE (file)); g_assert (G_IS_FILE_MONITOR (monitor)); if (g_cancellable_is_cancelled (self->cancellable)) return; if (dzl_recursive_file_monitor_ignored (self, file)) return; if (event == G_FILE_MONITOR_EVENT_DELETED) { if (g_hash_table_contains (self->monitors_by_file, file)) dzl_recursive_file_monitor_unwatch (self, file); } else if (event == G_FILE_MONITOR_EVENT_CREATED) { if (g_file_query_file_type (file, 0, NULL) == G_FILE_TYPE_DIRECTORY) { g_autoptr(GPtrArray) dirs = NULL; dirs = g_ptr_array_new_with_free_func (g_object_unref); g_ptr_array_add (dirs, g_object_ref (file)); dzl_recursive_file_monitor_collect_recursive (dirs, file, self->cancellable); for (guint i = 0; i < dirs->len; i++) { g_autoptr(GFileMonitor) dir_monitor = NULL; GFile *dir = g_ptr_array_index (dirs, i); if (!!(dir_monitor = g_file_monitor_directory (dir, MONITOR_FLAGS, self->cancellable, NULL))) dzl_recursive_file_monitor_track (self, dir, dir_monitor); } } } g_signal_emit (self, signals [CHANGED], 0, file, other_file, event); } static void dzl_recursive_file_monitor_track (DzlRecursiveFileMonitor *self, GFile *dir, GFileMonitor *monitor) { dzl_assert_is_main_thread (); g_assert (DZL_IS_RECURSIVE_FILE_MONITOR (self)); g_assert (G_IS_FILE (dir)); g_assert (G_IS_FILE_MONITOR (monitor)); g_hash_table_insert (self->monitors_by_file, g_object_ref (dir), g_object_ref (monitor)); g_hash_table_insert (self->files_by_monitor, g_object_ref (monitor), g_object_ref (dir)); g_signal_connect_object (monitor, "changed", G_CALLBACK (dzl_recursive_file_monitor_changed), self, G_CONNECT_SWAPPED); } static void dzl_recursive_file_monitor_start_cb (GObject *object, GAsyncResult *result, gpointer user_data) { DzlRecursiveFileMonitor *self = (DzlRecursiveFileMonitor *)object; g_autoptr(GPtrArray) dirs = NULL; g_autoptr(GError) error = NULL; g_autoptr(GTask) task = user_data; dzl_assert_is_main_thread (); g_assert (DZL_IS_RECURSIVE_FILE_MONITOR (self)); g_assert (G_IS_ASYNC_RESULT (result)); g_assert (G_IS_TASK (task)); dirs = dzl_recursive_file_monitor_collect_finish (self, result, &error); if (dirs == NULL) { g_task_return_error (task, g_steal_pointer (&error)); return; } for (guint i = 0; i < dirs->len; i++) { GFile *dir = g_ptr_array_index (dirs, i); g_autoptr(GFileMonitor) monitor = NULL; g_assert (G_IS_FILE (dir)); if (dzl_recursive_file_monitor_ignored (self, dir)) { /* * Skip ahead to the next directory that does not have this directory * as a prefix. We can do this because we know the descendants are * guaranteed to immediately follow this directory. */ for (guint j = i + 1; j < dirs->len; j++, i++) { GFile *next = g_ptr_array_index (dirs, j); if (!g_file_has_prefix (next, dir)) break; } continue; } monitor = g_file_monitor_directory (dir, MONITOR_FLAGS, self->cancellable, &error); if (monitor == NULL) { g_warning ("Failed to monitor directory: %s", error->message); g_clear_error (&error); continue; } dzl_recursive_file_monitor_track (self, dir, monitor); } g_task_return_boolean (task, TRUE); } void dzl_recursive_file_monitor_start_async (DzlRecursiveFileMonitor *self, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data) { g_autoptr(GTask) task = NULL; g_return_if_fail (DZL_IS_RECURSIVE_FILE_MONITOR (self)); g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable)); task = g_task_new (self, cancellable, callback, user_data); g_task_set_source_tag (task, dzl_recursive_file_monitor_start_async); g_task_set_return_on_cancel (task, TRUE); g_task_set_task_data (task, g_object_ref (self->root), g_object_unref); g_task_set_priority (task, G_PRIORITY_LOW); if (self->root == NULL) { g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_INVAL, "Cannot start file monitor, no root directory set"); return; } dzl_recursive_file_monitor_collect (self, self->root, self->cancellable, dzl_recursive_file_monitor_start_cb, g_steal_pointer (&task)); } gboolean dzl_recursive_file_monitor_start_finish (DzlRecursiveFileMonitor *self, GAsyncResult *result, GError **error) { g_return_val_if_fail (DZL_IS_RECURSIVE_FILE_MONITOR (self), FALSE); g_return_val_if_fail (G_IS_TASK (result), FALSE); g_return_val_if_fail (g_task_is_valid (G_TASK (result), self), FALSE); return g_task_propagate_boolean (G_TASK (result), error); } static void dzl_recursive_file_monitor_constructed (GObject *object) { DzlRecursiveFileMonitor *self = (DzlRecursiveFileMonitor *)object; G_OBJECT_CLASS (dzl_recursive_file_monitor_parent_class)->constructed (object); if (self->root == NULL) g_warning ("%s created without a root directory", G_OBJECT_TYPE_NAME (self)); } static void dzl_recursive_file_monitor_dispose (GObject *object) { DzlRecursiveFileMonitor *self = (DzlRecursiveFileMonitor *)object; g_cancellable_cancel (self->cancellable); dzl_recursive_file_monitor_set_ignore_func (self, NULL, NULL, NULL); G_OBJECT_CLASS (dzl_recursive_file_monitor_parent_class)->dispose (object); } static void dzl_recursive_file_monitor_finalize (GObject *object) { DzlRecursiveFileMonitor *self = (DzlRecursiveFileMonitor *)object; g_clear_object (&self->root); g_clear_object (&self->cancellable); g_clear_pointer (&self->files_by_monitor, g_hash_table_unref); g_clear_pointer (&self->monitors_by_file, g_hash_table_unref); G_OBJECT_CLASS (dzl_recursive_file_monitor_parent_class)->finalize (object); } static void dzl_recursive_file_monitor_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { DzlRecursiveFileMonitor *self = DZL_RECURSIVE_FILE_MONITOR (object); switch (prop_id) { case PROP_ROOT: g_value_set_object (value, dzl_recursive_file_monitor_get_root (self)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); } } static void dzl_recursive_file_monitor_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { DzlRecursiveFileMonitor *self = DZL_RECURSIVE_FILE_MONITOR (object); switch (prop_id) { case PROP_ROOT: self->root = g_value_dup_object (value); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); } } static void dzl_recursive_file_monitor_class_init (DzlRecursiveFileMonitorClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->constructed = dzl_recursive_file_monitor_constructed; object_class->dispose = dzl_recursive_file_monitor_dispose; object_class->finalize = dzl_recursive_file_monitor_finalize; object_class->get_property = dzl_recursive_file_monitor_get_property; object_class->set_property = dzl_recursive_file_monitor_set_property; properties [PROP_ROOT] = g_param_spec_object ("root", "Root", "The root directory to monitor", G_TYPE_FILE, (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); g_object_class_install_properties (object_class, N_PROPS, properties); /** * DzlRecursiveFileMonitor::changed: * @self: a #DzlRecursiveFileMonitor * @file: a #GFile * @other_file: (nullable): a #GFile for the other file when applicable * @event: the #GFileMonitorEvent event * * This event is similar to #GFileMonitor::changed but can be fired from * any of the monitored directories in the recursive mount. * * Since: 3.28 */ signals [CHANGED] = g_signal_new ("changed", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, 3, G_TYPE_FILE, G_TYPE_FILE, G_TYPE_FILE_MONITOR_EVENT); } static void dzl_recursive_file_monitor_init (DzlRecursiveFileMonitor *self) { self->cancellable = g_cancellable_new (); self->files_by_monitor = g_hash_table_new_full (NULL, NULL, g_object_unref, g_object_unref); self->monitors_by_file = g_hash_table_new_full (g_file_hash, (GEqualFunc) g_file_equal, g_object_unref, g_object_unref); } DzlRecursiveFileMonitor * dzl_recursive_file_monitor_new (GFile *file) { g_return_val_if_fail (G_IS_FILE (file), NULL); return g_object_new (DZL_TYPE_RECURSIVE_FILE_MONITOR, "root", file, NULL); } /** * dzl_recursive_file_monitor_cancel: * @self: a #DzlRecursiveFileMonitor * * Cancels the recursive file monitor. * * Since: 3.28 */ void dzl_recursive_file_monitor_cancel (DzlRecursiveFileMonitor *self) { g_return_if_fail (DZL_IS_RECURSIVE_FILE_MONITOR (self)); g_object_run_dispose (G_OBJECT (self)); } /** * dzl_recursive_file_monitor_get_root: * @self: a #DzlRecursiveFileMonitor * * Gets the root directory used forthe file monitor. * * Returns: (transfer none): a #GFile * * Since: 3.28 */ GFile * dzl_recursive_file_monitor_get_root (DzlRecursiveFileMonitor *self) { g_return_val_if_fail (DZL_IS_RECURSIVE_FILE_MONITOR (self), NULL); return self->root; } /** * dzl_recursive_file_monitor_set_ignore_func: * @self: a #DzlRecursiveFileMonitor * @ignore_func: (scope async): a #DzlRecursiveIgnoreFunc * @ignore_func_data: closure data for @ignore_func * @ignore_func_data_destroy: destroy notify for @ignore_func_data * * Sets a callback function to determine if a #GFile should be ignored * from signal emission. * * @ignore_func will always be called from the applications main thread. * * If @ignore_func is %NULL, it is set to the default which does not * ignore any files or directories. * * Since: 3.28 */ void dzl_recursive_file_monitor_set_ignore_func (DzlRecursiveFileMonitor *self, DzlRecursiveIgnoreFunc ignore_func, gpointer ignore_func_data, GDestroyNotify ignore_func_data_destroy) { dzl_assert_is_main_thread (); g_return_if_fail (DZL_IS_RECURSIVE_FILE_MONITOR (self)); if (ignore_func == NULL) { ignore_func_data = NULL; ignore_func_data_destroy = NULL; } if (self->ignore_func_data && self->ignore_func_data_destroy) { gpointer data = self->ignore_func_data; GDestroyNotify notify = self->ignore_func_data_destroy; self->ignore_func = NULL; self->ignore_func_data = NULL; self->ignore_func_data_destroy = NULL; notify (data); } self->ignore_func = ignore_func; self->ignore_func_data = ignore_func_data; self->ignore_func_data_destroy = ignore_func_data_destroy; }