/* * c_watch.c - Watch and .Watch.Events classes * * Copyright (C) 2013, 2014 Tobias Boege * * 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 1, 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, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301, USA. */ #define __C_WATCH_C #include #include #include #include #include #include #include #include #include "main.h" #include "c_watch.h" //#define DEBUG_ME 1 #define GB_ErrorErrno() GB.Error(strerror(errno)) enum { EV_READ, EV_STAT, EV_CLOSE, EV_CREATE, EV_DELETE, EV_WRITE, EV_MOVE, EV_MOVED_FROM, EV_MOVED_TO, EV_OPEN, NUM_EV }; typedef struct cinfo CINFO; struct cinfo { struct inotify_event *iev; CINFO *prev; }; typedef struct { int fd; GB_HASHTABLE watches; CINFO *top; } CINOTIFY; typedef struct { CWATCH *root; char *path; int wd; int events[NUM_EV]; } WATCH_LIST; typedef struct { int *eventp; uint32_t mask; } EVENT_TABLE; /* XXX: We currently only support one inotify instance */ static CINOTIFY _ino = {-1, NULL, NULL}; DECLARE_EVENT(EVENT_Read); DECLARE_EVENT(EVENT_Stat); DECLARE_EVENT(EVENT_Close); DECLARE_EVENT(EVENT_Create); DECLARE_EVENT(EVENT_Delete); DECLARE_EVENT(EVENT_Write); DECLARE_EVENT(EVENT_Move); DECLARE_EVENT(EVENT_MoveFrom); DECLARE_EVENT(EVENT_MoveTo); DECLARE_EVENT(EVENT_Open); static EVENT_TABLE _event_table[] = { {&EVENT_Read, IN_ACCESS}, {&EVENT_Stat, IN_ATTRIB}, {&EVENT_Close, IN_CLOSE_WRITE | IN_CLOSE_NOWRITE}, {&EVENT_Create, IN_CREATE}, {&EVENT_Delete, IN_DELETE | IN_DELETE_SELF}, {&EVENT_Write, IN_MODIFY}, {&EVENT_Move, IN_MOVE_SELF}, {&EVENT_MoveFrom, IN_MOVED_FROM}, {&EVENT_MoveTo, IN_MOVED_TO}, {&EVENT_Open, IN_OPEN} }; static bool destroy_watch(CWATCH *watch); static void callback(int fd, int flags, CINOTIFY *ino); static void init_inotify(void) { if (_ino.fd >= 0) return; #if DEBUG_ME fprintf(stderr, "init_inotify\n"); #endif _ino.fd = inotify_init(); if (_ino.fd == -1) { GB_ErrorErrno(); return; } GB.HashTable.New(&_ino.watches, GB_COMP_BINARY); GB.Watch(_ino.fd, GB_WATCH_READ, callback, (intptr_t) &_ino); _ino.top = NULL; } static void destroy_watch_list(WATCH_LIST *list) { while (!destroy_watch(list->root)); } static void exit_inotify(void) { WATCH_LIST *list; int fd = _ino.fd; // prevent a recursion if (_ino.fd < 0) return; _ino.fd = -1; #if DEBUG_ME fprintf(stderr, "exit_inotify\n"); #endif while (!GB.HashTable.First(_ino.watches, (void **)&list)) destroy_watch_list(list); GB.Watch(fd, GB_WATCH_NONE, NULL, 0); close(fd); GB.HashTable.Free(&_ino.watches); } static WATCH_LIST *find_watch_list(CINOTIFY *ino, int wd) { WATCH_LIST *list = NULL; GB.HashTable.Get(ino->watches, (char *)&wd, sizeof(wd), (void **)&list); return list; } static WATCH_LIST *find_watch_list_from_path(CINOTIFY *ino, const char *path, int len, bool create) { WATCH_LIST *list = NULL; GB.HashTable.Get(ino->watches, path, len, (void **)&list); if (!list && create) { #if DEBUG_ME fprintf(stderr, "find_watch_list_from_path: create watch list for %.*s\n", len, path); #endif GB.AllocZero(POINTER(&list), sizeof(WATCH_LIST)); list->wd = -1; list->path = GB.NewString(path, len); GB.HashTable.Add(ino->watches, path, len, list); } return list; } static void update_watch_list(WATCH_LIST *list) { int i; uint32_t mask = 0; for (i = 0; i < NUM_EV; i++) { if (list->events[i]) mask |= _event_table[i].mask; } if (mask == 0) { if (list->wd >= 0) { #if DEBUG_ME fprintf(stderr, "update_watch_list: remove watch %d\n", list->wd); #endif GB.HashTable.Remove(_ino.watches, (char *)&list->wd, sizeof(list->wd)); inotify_rm_watch(_ino.fd, list->wd); list->wd = -1; } } else { int wd = inotify_add_watch(_ino.fd, list->path, mask); if (wd >= 0 && list->wd != wd) { #if DEBUG_ME fprintf(stderr, "update_watch_list: add watch %d %08X\n", wd, mask); #endif list->wd = wd; GB.HashTable.Add(_ino.watches, (char *)&wd, sizeof(wd), list); } } } static void create_watch(CWATCH *watch, const char *path, int len) { int i; WATCH_LIST *list = find_watch_list_from_path(&_ino, path, len, TRUE); watch->root = list; #if DEBUG_ME fprintf(stderr, "create_watch: %p %.*s\n", watch, len, path); #endif LIST_insert(&list->root, watch, &watch->list); for (i = 0; i < NUM_EV; i++) { if (watch->events & (1 << i)) list->events[i]++; } update_watch_list(list); } static bool destroy_watch(CWATCH *watch) { int i; WATCH_LIST *list = (WATCH_LIST *)watch->root; if (!list) return FALSE; watch->root = NULL; #if DEBUG_ME fprintf(stderr, "destroy_watch: %p %s\n", watch, list->path); #endif GB.StoreVariant(NULL, &watch->tag); LIST_remove(&list->root, watch, &watch->list); for (i = 0; i < NUM_EV; i++) { if (watch->events & (1 << i)) list->events[i]--; } update_watch_list(list); if (list->root == NULL) { GB.HashTable.Remove(_ino.watches, list->path, GB.StringLength(list->path)); #if DEBUG_ME fprintf(stderr, "destroy_watch: remove watch list: %s -> %d\n", list->path, GB.HashTable.Count(_ino.watches)); #endif GB.FreeString(&list->path); GB.Free(POINTER(&list)); if (GB.HashTable.Count(_ino.watches) == 0) exit_inotify(); return TRUE; } else return FALSE; } static void pause_watch(CWATCH *watch) { int i; WATCH_LIST *list = (WATCH_LIST *)watch->root; if (watch->paused) return; watch->paused = TRUE; watch->save_events = watch->events; for (i = 0; i < NUM_EV; i++) { if (watch->events & (1 << i)) list->events[i]--; } watch->events = 0; update_watch_list(list); } static void resume_watch(CWATCH *watch) { int i; WATCH_LIST *list = (WATCH_LIST *)watch->root; if (!watch->paused) return; watch->paused = FALSE; watch->events = watch->save_events; for (i = 0; i < NUM_EV; i++) { if (watch->events & (1 << i)) list->events[i]++; } watch->save_events = 0; update_watch_list(list); } static void callback(int fd, int flags, CINOTIFY *ino) { struct inotify_event *iev; int i, j, length; int event; CINFO info; WATCH_LIST *list; CWATCH *watch; uint32_t mask; char buf[sizeof(*iev) + NAME_MAX + 1] __attribute__((aligned(sizeof(int)))); for(;;) { if ((length = read(fd, buf, sizeof(buf))) <= 0) { if (errno == EINTR) continue; GB_ErrorErrno(); return; } break; } for (i = 0; i < length; i += sizeof(*iev) + iev->len) { iev = (struct inotify_event *) &buf[i]; list = find_watch_list(ino, iev->wd); if (!list && !(iev->mask & IN_Q_OVERFLOW)) { if (getenv("GB_INOTIFY_DEBUG")) fprintf(stderr, "gb.inotify: descriptor %d not known. Name was `%s'\n", iev->wd, iev->name); continue; } mask = iev->mask & ~(IN_IGNORED | IN_ISDIR | IN_Q_OVERFLOW | IN_UNMOUNT); LIST_for_each(watch, list->root) { if (watch->paused) continue; for (j = 0; j < NUM_EV; j++) { event = *_event_table[j].eventp; if (_event_table[j].mask & mask && GB.CanRaise(watch, event)) { /* * In case, the event loop kicks in at GB.Raise() * and preempts this callback with itself. * * The problem is then that the event info structure * may get replaced by another, thus we lose data. * That's why we save it here. However, it is * sufficient to keep the CINFO on the stack! */ info.iev = iev; info.prev = _ino.top; _ino.top = &info; GB.Raise(watch, event, 0); _ino.top = info.prev; } } } /* Also remove the watch if the kernel did. */ /*if (iev->mask & IN_IGNORED) destroy_watch(watch);*/ } } /**G * Tidy up. **/ BEGIN_METHOD_VOID(Watch_exit) /* Free the remaining objects. destroy_watch() will then also take * care of calling INOTIFY_exit() to free bookkeeping data. */ exit_inotify(); END_METHOD #if 0 /**G * Return a Watch from its path. If the path is not watched, Null is * returned. **/ BEGIN_METHOD(Watch_get, GB_STRING path) CWATCH *watch; watch = find_watch_path(&_ino, GB.ToZeroString(ARG(path))); if (!watch) { GB.ReturnNull(); return; } GB.ReturnObject(watch); END_METHOD #endif /**G * The name of the file or directory which is subject to the event, relative * to the Watch object which triggered the event. If Null, the directory * itself is subject. **/ BEGIN_PROPERTY(Watch_Name) struct inotify_event *iev = _ino.top->iev; GB.ReturnNewZeroString(iev->len ? iev->name : ""); END_PROPERTY /**G * Return whether the event subject is/was a directory. **/ BEGIN_PROPERTY(Watch_IsDir) GB.ReturnBoolean(!!(_ino.top->iev->mask & IN_ISDIR)); END_PROPERTY /**G * Return whether the filesystem on which the watched path resided, was * unmounted. In this case, the Watch object is invalidated just after the * event. **/ BEGIN_PROPERTY(Watch_Unmount) GB.ReturnBoolean(!!(_ino.top->iev->mask & IN_UNMOUNT)); END_PROPERTY /**G * A cookie used to associate events. This is currently only used to connect * MoveFrom and MoveTo events of the same file. **/ BEGIN_PROPERTY(Watch_Cookie) GB.ReturnInteger(_ino.top->iev->cookie); END_PROPERTY #define THIS ((CWATCH *) _object) /**G * Create a new watch for the given path. * * If the NoFollowLink parameter is set and true, symlinks are not followed. * * Events is a bitmask specifying which events are to be reported at all. * The default mask is determined by the event handlers defined for this * object. * * TODO: Maybe add support for IN_EXCL_UNLINK, IN_ONESHOT and IN_ONLYDIR. **/ BEGIN_METHOD(Watch_new, GB_STRING path; GB_BOOLEAN nofollow; GB_INTEGER events) int i; ushort events = VARGOPT(events, 0); if (LENGTH(path) == 0) { GB.Error("Null path"); return; } //fprintf(stderr, "Watch_new: %p\n", THIS); /* If this is the first watch, we need an inotify instance first. * We don't use the component's init function to set up _ino because * the inotify instance needs a GB.Watch() which would keep the * program open even if no watches were registered. So this is done * ad-hoc if there are watches. */ init_inotify(); /* Get the mask */ if (!events) { for (i = 0; i < NUM_EV; i++) { if (GB.CanRaise(THIS, *_event_table[i].eventp)) events |= (1 << i); //_event_table[i].mask; } /* But we need at least to watch one event. IN_DELETE_SELF * seems a good one as it doesn't trigger too often :-) */ //mask |= IN_DELETE_SELF; } THIS->events = events; THIS->nofollow = VARGOPT(nofollow, FALSE); THIS->tag.type = GB_T_NULL; create_watch(THIS, STRING(path), LENGTH(path)); END_METHOD /**G * Deallocate the watch. **/ BEGIN_METHOD_VOID(Watch_free) //fprintf(stderr, "Watch_free: %p\n", THIS); destroy_watch(THIS); END_METHOD BEGIN_METHOD_VOID(Watch_Resume) resume_watch(THIS); END_METHOD BEGIN_METHOD_VOID(Watch_Pause) pause_watch(THIS); END_METHOD /**G * Return the watched path. **/ BEGIN_PROPERTY(Watch_Path) GB.ReturnString(((WATCH_LIST *)THIS->root)->path); END_PROPERTY /**G * Return or set if the watch is paused. * * {seealso * Pause(), Resume() * } **/ BEGIN_PROPERTY(Watch_IsPaused) if (READ_PROPERTY) GB.ReturnBoolean(THIS->paused); else if (THIS->paused != VPROP(GB_BOOLEAN)) { if (VPROP(GB_BOOLEAN)) pause_watch(THIS); else resume_watch(THIS); } END_PROPERTY /**G * This Variant is free for use by the Gambas programmer. **/ BEGIN_PROPERTY(Watch_Tag) if (READ_PROPERTY) { GB.ReturnVariant(&THIS->tag); return; } GB.StoreVariant(PROP(GB_VARIANT), &THIS->tag); END_PROPERTY GB_DESC CWatch[] = { GB_DECLARE("Watch", sizeof(CWATCH)), GB_CONSTANT("Read", "i", 1 << EV_READ), GB_CONSTANT("Stat", "i", 1 << EV_STAT), GB_CONSTANT("Close", "i", 1 << EV_CLOSE), GB_CONSTANT("Create", "i", 1 << EV_CREATE), GB_CONSTANT("Delete", "i", 1 << EV_DELETE), GB_CONSTANT("Write", "i", 1 << EV_WRITE), GB_CONSTANT("Move", "i", 1 << EV_MOVE), GB_CONSTANT("MoveFrom", "i", 1 << EV_MOVED_FROM), GB_CONSTANT("MoveTo", "i", 1 << EV_MOVED_TO), GB_CONSTANT("Open", "i", 1 << EV_OPEN), GB_CONSTANT("All", "i", -1), GB_EVENT("Read", NULL, NULL, &EVENT_Read), GB_EVENT("Stat", NULL, NULL, &EVENT_Stat), GB_EVENT("Close", NULL, NULL, &EVENT_Close), GB_EVENT("Create", NULL, NULL, &EVENT_Create), GB_EVENT("Delete", NULL, NULL, &EVENT_Delete), GB_EVENT("Write", NULL, NULL, &EVENT_Write), GB_EVENT("Move", NULL, NULL, &EVENT_Move), GB_EVENT("MoveFrom", NULL, NULL, &EVENT_MoveFrom), GB_EVENT("MoveTo", NULL, NULL, &EVENT_MoveTo), GB_EVENT("Open", NULL, NULL, &EVENT_Open), GB_STATIC_METHOD("_exit", NULL, Watch_exit, NULL), //GB_STATIC_METHOD("_get", "Watch", Watch_get, "(Path)s"), GB_METHOD("Resume", NULL, Watch_Resume, NULL), GB_METHOD("Pause", NULL, Watch_Pause, NULL), GB_STATIC_PROPERTY_READ("Name", "s", Watch_Name), GB_STATIC_PROPERTY_READ("IsDir", "b", Watch_IsDir), GB_STATIC_PROPERTY_READ("Unmount", "b", Watch_Unmount), GB_STATIC_PROPERTY_READ("Cookie", "i", Watch_Cookie), GB_METHOD("_new", NULL, Watch_new, "(Path)s[(NoFollowLink)b(Events)i]"), GB_METHOD("_free", NULL, Watch_free, NULL), GB_PROPERTY_SELF("Events", ".Watch.Events"), GB_PROPERTY_READ("Path", "s", Watch_Path), GB_PROPERTY("IsPaused", "b", Watch_IsPaused), GB_PROPERTY("Tag", "v", Watch_Tag), GB_END_DECLARE }; /**G * Return if the given flag is set. You can also combine multiple flags to * check if at least one of them is set. **/ BEGIN_METHOD(WatchEvents_get, GB_INTEGER events) GB.ReturnBoolean(!!((THIS->paused ? THIS->save_events : THIS->events) & VARG(events))); END_METHOD /**G * Set or clear a flag. You can combine multiple flags to set or clear them * en masse. **/ BEGIN_METHOD(WatchEvents_put, GB_BOOLEAN value; GB_INTEGER events) WATCH_LIST *list = (WATCH_LIST *)THIS->root; int i; int events = VARG(events); bool old, new; if (!THIS->paused) { for (i = 0; i < NUM_EV; i++) { new = (events & (1 << i)) != 0; old = (THIS->events & (1 << i)) != 0; if (new != old) { if (new) list->events[i]++; else list->events[i]--; } } THIS->events = events; update_watch_list(list); } else THIS->save_events = events; END_METHOD GB_DESC CWatchEvents[] = { GB_DECLARE_VIRTUAL(".Watch.Events"), GB_METHOD("_get", "b", WatchEvents_get, "(Events)i"), GB_METHOD("_put", NULL, WatchEvents_put, "(Value)b(Events)i"), GB_END_DECLARE };