/* Marmot * Copyright (C) 2003 James Willcox, Corey Bowers * * 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 "server_config.h" #define _GNU_SOURCE #include #include #include #include #include #include "gam_error.h" #include "gam_poll_dnotify.h" #include "gam_dnotify.h" #include "gam_tree.h" #include "gam_event.h" #include "gam_server.h" #include "gam_event.h" #ifdef GAMIN_DEBUG_API #include "gam_debugging.h" #endif /* just pulling a value out of nowhere here...may need tweaking */ #define MAX_QUEUE_SIZE 500 typedef struct { char *path; int fd; int refcount; int busy; } DNotifyData; static GHashTable *path_hash = NULL; static GHashTable *fd_hash = NULL; G_LOCK_DEFINE_STATIC(dnotify); /* TODO: GQueue is not signal-safe, need to use something else */ static GQueue *changes = NULL; static GIOChannel *pipe_read_ioc = NULL; static GIOChannel *pipe_write_ioc = NULL; static void gam_dnotify_data_debug (gpointer key, gpointer value, gpointer user_data) { int deactivated; DNotifyData *data = (DNotifyData *)value; if (!data) return; deactivated = data->fd == -1 ? 1 : 0; GAM_DEBUG(DEBUG_INFO, "dsub fd %d refs %d busy %d deactivated %d: %s\n", data->fd, data->refcount, data->busy, deactivated, data->path); } void gam_dnotify_debug () { if (path_hash == NULL) return; GAM_DEBUG(DEBUG_INFO, "Dumping dnotify subscriptions\n"); g_hash_table_foreach (path_hash, gam_dnotify_data_debug, NULL); } static DNotifyData * gam_dnotify_data_new(const char *path, int fd) { DNotifyData *data; data = g_new0(DNotifyData, 1); data->path = g_strdup(path); data->fd = fd; data->busy = 0; data->refcount = 1; return data; } static void gam_dnotify_data_free(DNotifyData * data) { g_free(data->path); g_free(data); } static void gam_dnotify_directory_handler_internal(const char *path, pollHandlerMode mode) { DNotifyData *data; int fd; switch (mode) { case GAMIN_ACTIVATE: GAM_DEBUG(DEBUG_INFO, "Adding %s to dnotify\n", path); break; case GAMIN_DEACTIVATE: GAM_DEBUG(DEBUG_INFO, "Removing %s from dnotify\n", path); break; case GAMIN_FLOWCONTROLSTART: GAM_DEBUG(DEBUG_INFO, "Start flow control for %s\n", path); break; case GAMIN_FLOWCONTROLSTOP: GAM_DEBUG(DEBUG_INFO, "Stop flow control for %s\n", path); break; default: gam_error(DEBUG_INFO, "Unknown DNotify operation %d for %s\n", mode, path); return; } G_LOCK(dnotify); if (mode == GAMIN_ACTIVATE) { if ((data = g_hash_table_lookup(path_hash, path)) != NULL) { data->refcount++; GAM_DEBUG(DEBUG_INFO, " found incremented refcount: %d\n", data->refcount); G_UNLOCK(dnotify); #ifdef GAMIN_DEBUG_API gam_debug_report(GAMDnotifyChange, path, data->refcount); #endif return; } fd = open(path, O_RDONLY); if (fd < 0) { G_UNLOCK(dnotify); return; } data = gam_dnotify_data_new(path, fd); g_hash_table_insert(fd_hash, GINT_TO_POINTER(data->fd), data); g_hash_table_insert(path_hash, data->path, data); fcntl(fd, F_SETSIG, SIGRTMIN); fcntl(fd, F_NOTIFY, DN_MODIFY | DN_CREATE | DN_DELETE | DN_RENAME | DN_ATTRIB | DN_MULTISHOT); GAM_DEBUG(DEBUG_INFO, "activated DNotify for %s\n", path); #ifdef GAMIN_DEBUG_API gam_debug_report(GAMDnotifyCreate, path, 0); #endif } else if (mode == GAMIN_DEACTIVATE) { data = g_hash_table_lookup(path_hash, path); if (!data) { GAM_DEBUG(DEBUG_INFO, " not found !!!\n"); G_UNLOCK(dnotify); return; } data->refcount--; if (data->refcount == 0) { close(data->fd); GAM_DEBUG(DEBUG_INFO, "deactivated DNotify for %s\n", data->path); g_hash_table_remove(path_hash, data->path); g_hash_table_remove(fd_hash, GINT_TO_POINTER(data->fd)); gam_dnotify_data_free(data); #ifdef GAMIN_DEBUG_API gam_debug_report(GAMDnotifyDelete, path, 0); #endif } else { GAM_DEBUG(DEBUG_INFO, " found decremented refcount: %d\n", data->refcount); #ifdef GAMIN_DEBUG_API gam_debug_report(GAMDnotifyChange, data->path, data->refcount); #endif } } else if ((mode == GAMIN_FLOWCONTROLSTART) || (mode == GAMIN_FLOWCONTROLSTOP)) { data = g_hash_table_lookup(path_hash, path); if (!data) { GAM_DEBUG(DEBUG_INFO, " not found !!!\n"); G_UNLOCK(dnotify); return; } if (data != NULL) { if (mode == GAMIN_FLOWCONTROLSTART) { if (data->fd >= 0) { close(data->fd); g_hash_table_remove(fd_hash, GINT_TO_POINTER(data->fd)); data->fd = -1; GAM_DEBUG(DEBUG_INFO, "deactivated DNotify for %s\n", data->path); #ifdef GAMIN_DEBUG_API gam_debug_report(GAMDnotifyFlowOn, data->path, 0); #endif } data->busy++; } else { if (data->busy > 0) { data->busy--; if (data->busy == 0) { fd = open(data->path, O_RDONLY); if (fd < 0) { G_UNLOCK(dnotify); GAM_DEBUG(DEBUG_INFO, "Failed to reactivate DNotify for %s\n", data->path); return; } data->fd = fd; g_hash_table_insert(fd_hash, GINT_TO_POINTER(data->fd), data); fcntl(fd, F_SETSIG, SIGRTMIN); fcntl(fd, F_NOTIFY, DN_MODIFY | DN_CREATE | DN_DELETE | DN_RENAME | DN_ATTRIB | DN_MULTISHOT); GAM_DEBUG(DEBUG_INFO, "Reactivated DNotify for %s\n", data->path); #ifdef GAMIN_DEBUG_API gam_debug_report(GAMDnotifyFlowOff, path, 0); #endif } } } } } else { GAM_DEBUG(DEBUG_INFO, "Unimplemented operation\n"); } G_UNLOCK(dnotify); } static void gam_dnotify_directory_handler(const char *path, pollHandlerMode mode) { GAM_DEBUG(DEBUG_INFO, "gam_dnotify_directory_handler %s : %d\n", path, mode); gam_dnotify_directory_handler_internal(path, mode); } static void gam_dnotify_file_handler(const char *path, pollHandlerMode mode) { GAM_DEBUG(DEBUG_INFO, "gam_dnotify_file_handler %s : %d\n", path, mode); if (g_file_test(path, G_FILE_TEST_IS_DIR)) { gam_dnotify_directory_handler_internal(path, mode); } else { GAM_DEBUG(DEBUG_INFO, " not a dir %s, failed !!!\n", path); } } static void dnotify_signal_handler(int sig, siginfo_t * si, void *sig_data) { if (changes->length > MAX_QUEUE_SIZE) { GAM_DEBUG(DEBUG_INFO, "Queue Full\n"); return; } g_queue_push_head(changes, GINT_TO_POINTER(si->si_fd)); g_io_channel_write_chars(pipe_write_ioc, "bogus", 5, NULL, NULL); g_io_channel_flush(pipe_write_ioc, NULL); GAM_DEBUG(DEBUG_INFO, "signal handler done\n"); } static void overflow_signal_handler(int sig, siginfo_t * si, void *sig_data) { GAM_DEBUG(DEBUG_INFO, "**** signal queue overflow ***\n"); } static gboolean gam_dnotify_pipe_handler(gpointer user_data) { char buf[5000]; DNotifyData *data; gpointer fd; int i; GAM_DEBUG(DEBUG_INFO, "gam_dnotify_pipe_handler()\n"); g_io_channel_read_chars(pipe_read_ioc, buf, sizeof(buf), NULL, NULL); i = 0; while ((fd = g_queue_pop_tail(changes)) != NULL) { G_LOCK(dnotify); data = g_hash_table_lookup(fd_hash, fd); G_UNLOCK(dnotify); if (data == NULL) continue; GAM_DEBUG(DEBUG_INFO, "handling signal\n"); gam_poll_generic_scan_directory(data->path); i++; } GAM_DEBUG(DEBUG_INFO, "gam_dnotify_pipe_handler() done\n"); return TRUE; } /** * @defgroup DNotify DNotify Backend * @ingroup Backends * @brief DNotify backend API * * Since version 2.4, Linux kernels have included the Linux Directory * Notification system (dnotify). This backend uses dnotify to know when * files are changed/created/deleted. Since dnotify doesn't tell us * exactly what event happened to which file (just that some even happened * in some directory), we still have to cache stat() information. For this, * we can just use the code in the polling backend. * * @{ */ /** * Initializes the polling system. This must be called before * any other functions in this module. * * @returns TRUE if initialization succeeded, FALSE otherwise */ gboolean gam_dnotify_init(void) { struct sigaction act; int fds[2]; GSource *source; g_return_val_if_fail(gam_poll_dnotify_init (), FALSE); if (pipe(fds) < 0) { g_warning("Could not create pipe.\n"); return FALSE; } pipe_read_ioc = g_io_channel_unix_new(fds[0]); pipe_write_ioc = g_io_channel_unix_new(fds[1]); g_io_channel_set_flags(pipe_read_ioc, G_IO_FLAG_NONBLOCK, NULL); g_io_channel_set_flags(pipe_write_ioc, G_IO_FLAG_NONBLOCK, NULL); source = g_io_create_watch(pipe_read_ioc, G_IO_IN | G_IO_HUP | G_IO_ERR); g_source_set_callback(source, gam_dnotify_pipe_handler, NULL, NULL); g_source_attach(source, NULL); g_source_unref(source); /* setup some signal stuff */ act.sa_sigaction = dnotify_signal_handler; sigemptyset(&act.sa_mask); act.sa_flags = SA_SIGINFO; sigaction(SIGRTMIN, &act, NULL); /* catch SIGIO as well (happens when the realtime queue fills up) */ act.sa_sigaction = overflow_signal_handler; sigemptyset(&act.sa_mask); sigaction(SIGIO, &act, NULL); changes = g_queue_new(); path_hash = g_hash_table_new(g_str_hash, g_str_equal); fd_hash = g_hash_table_new(g_direct_hash, g_direct_equal); GAM_DEBUG(DEBUG_INFO, "dnotify initialized\n"); gam_server_install_kernel_hooks (GAMIN_K_DNOTIFY, gam_dnotify_add_subscription, gam_dnotify_remove_subscription, gam_dnotify_remove_all_for, gam_dnotify_directory_handler, gam_dnotify_file_handler); 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_dnotify_add_subscription(GamSubscription * sub) { GAM_DEBUG(DEBUG_INFO, "dnotify: Adding subscription for %s\n", gam_subscription_get_path (sub)); if (!gam_poll_add_subscription(sub)) { return FALSE; } 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_dnotify_remove_subscription(GamSubscription * sub) { GAM_DEBUG(DEBUG_INFO, "dnotify: Removing subscription for %s\n", gam_subscription_get_path (sub)); if (!gam_poll_remove_subscription(sub)) { return FALSE; } 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_dnotify_remove_all_for(GamListener * listener) { GAM_DEBUG(DEBUG_INFO, "dnotify: Removing all subscriptions for %s\n", gam_listener_get_pidname (listener)); if (!gam_poll_remove_all_for(listener)) { return FALSE; } return TRUE; } /** @} */