/* * QEMU file monitor Linux inotify impl * * Copyright (c) 2018 Red Hat, Inc. * * 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, see . * */ #include "qemu/osdep.h" #include "qemu/filemonitor.h" #include "qemu/main-loop.h" #include "qemu/error-report.h" #include "qapi/error.h" #include "trace.h" #include struct QFileMonitor { int fd; QemuMutex lock; /* protects dirs & idmap */ GHashTable *dirs; /* dirname => QFileMonitorDir */ GHashTable *idmap; /* inotify ID => dirname */ }; typedef struct { int id; /* watch ID */ char *filename; /* optional filter */ QFileMonitorHandler cb; void *opaque; } QFileMonitorWatch; typedef struct { char *path; int id; /* inotify ID */ int nextid; /* watch ID counter */ GArray *watches; /* QFileMonitorWatch elements */ } QFileMonitorDir; static void qemu_file_monitor_watch(void *arg) { QFileMonitor *mon = arg; char buf[4096] __attribute__ ((aligned(__alignof__(struct inotify_event)))); int used = 0; int len; qemu_mutex_lock(&mon->lock); if (mon->fd == -1) { qemu_mutex_unlock(&mon->lock); return; } len = read(mon->fd, buf, sizeof(buf)); if (len < 0) { if (errno != EAGAIN) { error_report("Failure monitoring inotify FD '%s'," "disabling events", strerror(errno)); goto cleanup; } /* no more events right now */ goto cleanup; } /* Loop over all events in the buffer */ while (used < len) { struct inotify_event *ev = (struct inotify_event *)(buf + used); const char *name = ev->len ? ev->name : ""; QFileMonitorDir *dir = g_hash_table_lookup(mon->idmap, GINT_TO_POINTER(ev->wd)); uint32_t iev = ev->mask & (IN_CREATE | IN_MODIFY | IN_DELETE | IN_IGNORED | IN_MOVED_TO | IN_MOVED_FROM | IN_ATTRIB); int qev; gsize i; used += sizeof(struct inotify_event) + ev->len; if (!dir) { continue; } /* * During a rename operation, the old name gets * IN_MOVED_FROM and the new name gets IN_MOVED_TO. * To simplify life for callers, we turn these into * DELETED and CREATED events */ switch (iev) { case IN_CREATE: case IN_MOVED_TO: qev = QFILE_MONITOR_EVENT_CREATED; break; case IN_MODIFY: qev = QFILE_MONITOR_EVENT_MODIFIED; break; case IN_DELETE: case IN_MOVED_FROM: qev = QFILE_MONITOR_EVENT_DELETED; break; case IN_ATTRIB: qev = QFILE_MONITOR_EVENT_ATTRIBUTES; break; case IN_IGNORED: qev = QFILE_MONITOR_EVENT_IGNORED; break; default: g_assert_not_reached(); } trace_qemu_file_monitor_event(mon, dir->path, name, ev->mask, dir->id); for (i = 0; i < dir->watches->len; i++) { QFileMonitorWatch *watch = &g_array_index(dir->watches, QFileMonitorWatch, i); if (watch->filename == NULL || (name && g_str_equal(watch->filename, name))) { trace_qemu_file_monitor_dispatch(mon, dir->path, name, qev, watch->cb, watch->opaque, watch->id); watch->cb(watch->id, qev, name, watch->opaque); } } } cleanup: qemu_mutex_unlock(&mon->lock); } static void qemu_file_monitor_dir_free(void *data) { QFileMonitorDir *dir = data; gsize i; for (i = 0; i < dir->watches->len; i++) { QFileMonitorWatch *watch = &g_array_index(dir->watches, QFileMonitorWatch, i); g_free(watch->filename); } g_array_unref(dir->watches); g_free(dir->path); g_free(dir); } QFileMonitor * qemu_file_monitor_new(Error **errp) { int fd; QFileMonitor *mon; fd = inotify_init1(IN_NONBLOCK); if (fd < 0) { error_setg_errno(errp, errno, "Unable to initialize inotify"); return NULL; } mon = g_new0(QFileMonitor, 1); qemu_mutex_init(&mon->lock); mon->fd = fd; mon->dirs = g_hash_table_new_full(g_str_hash, g_str_equal, NULL, qemu_file_monitor_dir_free); mon->idmap = g_hash_table_new(g_direct_hash, g_direct_equal); trace_qemu_file_monitor_new(mon, mon->fd); return mon; } static gboolean qemu_file_monitor_free_idle(void *opaque) { QFileMonitor *mon = opaque; if (!mon) { return G_SOURCE_REMOVE; } qemu_mutex_lock(&mon->lock); g_hash_table_unref(mon->idmap); g_hash_table_unref(mon->dirs); qemu_mutex_unlock(&mon->lock); qemu_mutex_destroy(&mon->lock); g_free(mon); return G_SOURCE_REMOVE; } void qemu_file_monitor_free(QFileMonitor *mon) { if (!mon) { return; } qemu_mutex_lock(&mon->lock); if (mon->fd != -1) { qemu_set_fd_handler(mon->fd, NULL, NULL, NULL); close(mon->fd); mon->fd = -1; } qemu_mutex_unlock(&mon->lock); /* * Can't free it yet, because another thread * may be running event loop, so the inotify * callback might be pending. Using an idle * source ensures we'll only free after the * pending callback is done */ g_idle_add((GSourceFunc)qemu_file_monitor_free_idle, mon); } int qemu_file_monitor_add_watch(QFileMonitor *mon, const char *dirpath, const char *filename, QFileMonitorHandler cb, void *opaque, Error **errp) { QFileMonitorDir *dir; QFileMonitorWatch watch; int ret = -1; qemu_mutex_lock(&mon->lock); dir = g_hash_table_lookup(mon->dirs, dirpath); if (!dir) { int rv = inotify_add_watch(mon->fd, dirpath, IN_CREATE | IN_DELETE | IN_MODIFY | IN_MOVED_TO | IN_MOVED_FROM | IN_ATTRIB); if (rv < 0) { error_setg_errno(errp, errno, "Unable to watch '%s'", dirpath); goto cleanup; } trace_qemu_file_monitor_enable_watch(mon, dirpath, rv); dir = g_new0(QFileMonitorDir, 1); dir->path = g_strdup(dirpath); dir->id = rv; dir->watches = g_array_new(FALSE, TRUE, sizeof(QFileMonitorWatch)); g_hash_table_insert(mon->dirs, dir->path, dir); g_hash_table_insert(mon->idmap, GINT_TO_POINTER(rv), dir); if (g_hash_table_size(mon->dirs) == 1) { qemu_set_fd_handler(mon->fd, qemu_file_monitor_watch, NULL, mon); } } watch.id = dir->nextid++; watch.filename = g_strdup(filename); watch.cb = cb; watch.opaque = opaque; g_array_append_val(dir->watches, watch); trace_qemu_file_monitor_add_watch(mon, dirpath, filename ? filename : "", cb, opaque, watch.id); ret = watch.id; cleanup: qemu_mutex_unlock(&mon->lock); return ret; } void qemu_file_monitor_remove_watch(QFileMonitor *mon, const char *dirpath, int id) { QFileMonitorDir *dir; gsize i; qemu_mutex_lock(&mon->lock); trace_qemu_file_monitor_remove_watch(mon, dirpath, id); dir = g_hash_table_lookup(mon->dirs, dirpath); if (!dir) { goto cleanup; } for (i = 0; i < dir->watches->len; i++) { QFileMonitorWatch *watch = &g_array_index(dir->watches, QFileMonitorWatch, i); if (watch->id == id) { g_free(watch->filename); g_array_remove_index(dir->watches, i); break; } } if (dir->watches->len == 0) { inotify_rm_watch(mon->fd, dir->id); trace_qemu_file_monitor_disable_watch(mon, dir->path, dir->id); g_hash_table_remove(mon->idmap, GINT_TO_POINTER(dir->id)); g_hash_table_remove(mon->dirs, dir->path); if (g_hash_table_size(mon->dirs) == 0) { qemu_set_fd_handler(mon->fd, NULL, NULL, NULL); } } cleanup: qemu_mutex_unlock(&mon->lock); }