Message ID | 20181019133835.16494-2-berrange@redhat.com |
---|---|
State | New |
Headers | show |
Series | Add a standard authorization framework | expand |
Hi On Fri, Oct 19, 2018 at 5:41 PM Daniel P. Berrangé <berrange@redhat.com> wrote: > > The inotify userspace API for reading events is quite horrible, so it is > useful to wrap it in a more friendly API to avoid duplicating code > across many users in QEMU. Wrapping it also allows introduction of a > platform portability layer, so that we can add impls for non-Linux based > equivalents in future. > > Signed-off-by: Daniel P. Berrangé <berrange@redhat.com> > --- > include/qemu/filemonitor.h | 117 ++++++++++++++ > util/filemonitor.c | 315 +++++++++++++++++++++++++++++++++++++ > MAINTAINERS | 6 + > util/Makefile.objs | 1 + > util/trace-events | 9 ++ > 5 files changed, 448 insertions(+) > create mode 100644 include/qemu/filemonitor.h > create mode 100644 util/filemonitor.c > > diff --git a/include/qemu/filemonitor.h b/include/qemu/filemonitor.h > new file mode 100644 > index 0000000000..1326272f0a > --- /dev/null > +++ b/include/qemu/filemonitor.h > @@ -0,0 +1,117 @@ > +/* > + * QEMU file monitor helper > + * > + * 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 <http://www.gnu.org/licenses/>. > + * > + */ > + > +#ifndef QEMU_FILE_MONITOR_H > +#define QEMU_FILE_MONITOR_H > + > +#include "qemu-common.h" > + > + > +typedef struct QFileMonitor QFileMonitor; > + > +typedef enum { > + /* File has been created in a dir */ > + QFILE_MONITOR_EVENT_CREATED, > + /* File has been modified in a dir */ > + QFILE_MONITOR_EVENT_MODIFIED, > + /* File has been deleted in a dir */ > + QFILE_MONITOR_EVENT_DELETED, > + /* Dir is no longer being monitored (due to deletion) */ > + QFILE_MONITOR_EVENT_IGNORED, > +} QFileMonitorEvent; > + > + > +/** > + * QFileMonitorHandler: > + * @id: id from qemu_file_monitor_add_watch() > + * @event: the file change that occurred > + * @filename: the name of the file affected > + * @opaque: opaque data provided to qemu_file_monitor_add_watch() > + * > + * Invoked whenever a file changes. If @event is > + * QFILE_MONITOR_EVENT_IGNORED, @filename will be > + * empty. > + * > + */ > +typedef void (*QFileMonitorHandler)(int id, > + QFileMonitorEvent event, > + const char *filename, > + void *opaque); > + > +/** > + * qemu_file_monitor_get_instance: > + * @errp: pointer to a NULL-initialized error object > + * > + * Acquire a handle to the shared file monitoring object. > + * > + * This object does locking internally to enable it to be > + * safe to use from multiple threads > + * > + * If the platform does not support file monitoring, an > + * error will be reported. Likewise if file monitoring > + * is supported, but cannot be initialized > + * > + * Currently this is implemented on Linux platforms with > + * the inotify subsystem. > + * > + * Returns: the shared monitoring object, or NULL on error > + */ > +QFileMonitor *qemu_file_monitor_get_instance(Error **errp); > + > +/** > + * qemu_file_monitor_add_watch: > + * @mon: the file monitor context > + * @dirpath: the directory whose contents to watch > + * @filename: optional filename to filter on > + * @cb: the function to invoke when @dirpath has changes > + * @opaque: data to pass to @cb > + * @errp: pointer to a NULL-initialized error object > + * > + * Register to receive notifications of changes > + * in the directory @dirpath. All files in the > + * directory will be monitored. If the caller is > + * only interested in one specific file, @filename > + * can be used to filter events. > + * > + * Returns: a positive integer watch ID, or -1 on error > + */ > +int qemu_file_monitor_add_watch(QFileMonitor *mon, > + const char *dirpath, > + const char *filename, > + QFileMonitorHandler cb, > + void *opaque, > + Error **errp); > + > +/** > + * qemu_file_monitor_remove_watch: > + * @mon: the file monitor context > + * @dirpath: the directory whose contents to unwatch > + * @id: id of the watch to remove > + * > + * Removes the file monitoring watch @id, associated > + * with the directory @dirpath. This must never be > + * called from a QFileMonitorHandler callback, or a > + * deadlock will result. > + */ > +void qemu_file_monitor_remove_watch(QFileMonitor *mon, > + const char *dirpath, > + int id); > + > +#endif /* QEMU_FILE_MONITOR_H */ > diff --git a/util/filemonitor.c b/util/filemonitor.c > new file mode 100644 > index 0000000000..67d7aedbe0 > --- /dev/null > +++ b/util/filemonitor.c > @@ -0,0 +1,315 @@ > +/* > + * QEMU file_monitor helper > + * > + * 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 <http://www.gnu.org/licenses/>. > + * > + */ > + > +#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" > + > +struct QFileMonitor { > + QemuMutex lock; > + int fd; > + > + 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 */ > + gsize nwatches; > + QFileMonitorWatch *watches; > +} QFileMonitorDir; > + > + > +#ifdef CONFIG_INOTIFY1 > +#include <sys/inotify.h> > + > +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 = read(mon->fd, buf, sizeof(buf)); > + > + qemu_mutex_lock(&mon->lock); I suppose the lock should guard from mon->fd above, or there might be a race when modifying/removing a watch from a different thread. > + > + if (len < 0) { > + if (errno != EAGAIN) { > + error_report("Failure monitoring inotify FD, disabling events"); strerror(errno) could be useful > + 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); > + 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_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->nwatches; i++) { > + QFileMonitorWatch *watch = &dir->watches[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; > + > + g_free(dir->watches); for sake, I would add assert(dir->nwatches = 0) > + g_free(dir); > +} > + > +#endif > + > +static QFileMonitor * > +qemu_file_monitor_new(Error **errp) > +{ > +#ifdef CONFIG_INOTIFY1 > + 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; > +#else > + error_setg(errp, "File monitoring not available on this platform"); > + return NULL; > +#endif > +} > + > + > +QFileMonitor *qemu_file_monitor_get_instance(Error **errp) > +{ > + static QFileMonitor *global; > + > + if (!global) { > + global = qemu_file_monitor_new(errp); > + } > + > + return global; > +} > + > + > +#ifdef CONFIG_INOTIFY1 > +int > +qemu_file_monitor_add_watch(QFileMonitor *mon, > + const char *dirpath, > + const char *filename, > + QFileMonitorHandler cb, > + void *opaque, > + Error **errp) > +{ > + QFileMonitorDir *dir; > + 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); > + > + 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; > + > + 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); > + } > + } > + > + dir->watches = g_renew(QFileMonitorWatch, dir->watches, dir->nwatches + 1); GArray could eventually make handling of watches a bit simpler (counting, resizing, removing etc) > + > + dir->watches[dir->nwatches].id = ++dir->nextid; > + dir->watches[dir->nwatches].filename = filename ? g_strdup(filename) : NULL; g_strdup(NULL) returns NULL already > + dir->watches[dir->nwatches].cb = cb; > + dir->watches[dir->nwatches].opaque = opaque; > + dir->nwatches++; > + > + trace_qemu_file_monitor_add_watch(mon, dirpath, > + filename ? filename : "<none>", > + cb, opaque, > + dir->watches[dir->nwatches - 1].id); > + > + ret = 0; > + > + 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->nwatches; i++) { > + if (dir->watches[i].id == id) { > + if (i < (dir->nwatches - 1)) { > + memmove(dir->watches + i, > + dir->watches + i + 1, > + sizeof(QFileMonitorWatch) * > + (dir->nwatches - (i + 1))); > + dir->watches = g_renew(QFileMonitorWatch, dir->watches, > + dir->nwatches - 1); > + dir->nwatches--; > + } > + break; > + } > + } > + > + if (dir->nwatches == 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); > + } > + > + cleanup: > + qemu_mutex_lock(&mon->lock); > +} > + > +#else > +int > +qemu_file_monitor_add_watch(QFileMonitor *mon, > + const char *dirpath, > + const char *filename, > + QFileMonitorHandler cb, > + void *opaque, > + Error **errp) > +{ > + error_setg(errp, "File monitoring not available on this platform"); > + return -1; > +} > + > +void qemu_file_monitor_remove_watch(QFileMonitor *mon, > + const char *dirpath, > + int id) > +{ > +} Wouldn't it be cleaner with stubs/ ? > +#endif > + > diff --git a/MAINTAINERS b/MAINTAINERS > index 40672c4eba..29bbcf8c25 100644 > --- a/MAINTAINERS > +++ b/MAINTAINERS > @@ -1866,6 +1866,12 @@ F: include/qemu/sockets.h > F: util/qemu-sockets.c > F: qapi/sockets.json > > +File monitor > +M: Daniel P. Berrange <berrange@redhat.com> > +S: Odd fixes > +F: util/filemonitor.c > +F: include/qemu/filemonitor.h > + > Throttling infrastructure > M: Alberto Garcia <berto@igalia.com> > S: Supported > diff --git a/util/Makefile.objs b/util/Makefile.objs > index 0820923c18..4d7675d6e7 100644 > --- a/util/Makefile.objs > +++ b/util/Makefile.objs > @@ -50,5 +50,6 @@ util-obj-y += range.o > util-obj-y += stats64.o > util-obj-y += systemd.o > util-obj-y += iova-tree.o > +util-obj-y += filemonitor.o > util-obj-$(CONFIG_LINUX) += vfio-helpers.o > util-obj-$(CONFIG_OPENGL) += drm.o > diff --git a/util/trace-events b/util/trace-events > index 79569b7fdf..ff19b253e2 100644 > --- a/util/trace-events > +++ b/util/trace-events > @@ -21,6 +21,15 @@ buffer_move_empty(const char *buf, size_t len, const char *from) "%s: %zd bytes > buffer_move(const char *buf, size_t len, const char *from) "%s: %zd bytes from %s" > buffer_free(const char *buf, size_t len) "%s: capacity %zd" > > +# util/filemonitor.c > +qemu_file_monitor_add_watch(void *mon, const char *dirpath, const char *filename, void *cb, void *opaque, int id) "File monitor %p add watch dir='%s' file='%s' cb=%p opaque=%p id=%u" > +qemu_file_monitor_remove_watch(void *mon, const char *dirpath, int id) "File monitor %p remove watch dir='%s' id=%u" > +qemu_file_monitor_new(void *mon, int fd) "File monitor %p created fd=%d" > +qemu_file_monitor_enable_watch(void *mon, const char *dirpath, int id) "File monitor %p enable watch dir='%s' id=%u" > +qemu_file_monitor_disable_watch(void *mon, const char *dirpath, int id) "Fle monitor %p disable watch dir='%s' id=%u" > +qemu_file_monitor_event(void *mon, const char *dirpath, const char *filename, int mask, unsigned int id) "File monitor %p event dir='%s' file='%s' mask=0x%x id=%u" > +qemu_file_monitor_dispatch(void *mon, const char *dirpath, const char *filename, int ev, void *cb, void *opaque, unsigned int id) "File monitor %p dispatch dir='%s' file='%s' ev=%d cb=%p opaque=%p id=%u" > + > # util/qemu-coroutine.c > qemu_aio_coroutine_enter(void *ctx, void *from, void *to, void *opaque) "ctx %p from %p to %p opaque %p" > qemu_coroutine_yield(void *from, void *to) "from %p to %p" > -- > 2.17.2 > > Looks good, but would be even better with tests :)
On Wed, Nov 07, 2018 at 10:08:05PM +0400, Marc-André Lureau wrote: > Hi > > On Fri, Oct 19, 2018 at 5:41 PM Daniel P. Berrangé <berrange@redhat.com> wrote: > > > > The inotify userspace API for reading events is quite horrible, so it is > > useful to wrap it in a more friendly API to avoid duplicating code > > across many users in QEMU. Wrapping it also allows introduction of a > > platform portability layer, so that we can add impls for non-Linux based > > equivalents in future. > > > > Signed-off-by: Daniel P. Berrangé <berrange@redhat.com> > > --- > > +struct QFileMonitor { > > + QemuMutex lock; > > + int fd; > > + > > + 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 */ > > + gsize nwatches; > > + QFileMonitorWatch *watches; > > +} QFileMonitorDir; > > + > > + > > +#ifdef CONFIG_INOTIFY1 > > +#include <sys/inotify.h> > > + > > +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 = read(mon->fd, buf, sizeof(buf)); > > + > > + qemu_mutex_lock(&mon->lock); > > I suppose the lock should guard from mon->fd above, or there might be > a race when modifying/removing a watch from a different thread. The mutex is only there to protect the "dirs" and "idmap" hash tables. The "fd" has enough safety - the kernel is fine with you reading from the fd, while another thread is adding/removing files on the inotify handle. Since the QFileMonitor object is a singleton that can never be free'd we'll never race with closing 'fd either. > > > + > > + if (len < 0) { > > + if (errno != EAGAIN) { > > + error_report("Failure monitoring inotify FD, disabling events"); > > strerror(errno) could be useful Yep. > > +static void > > +qemu_file_monitor_dir_free(void *data) > > +{ > > + QFileMonitorDir *dir = data; > > + > > + g_free(dir->watches); > > for sake, I would add > assert(dir->nwatches = 0) Yep. > > +#ifdef CONFIG_INOTIFY1 > > +int > > +qemu_file_monitor_add_watch(QFileMonitor *mon, > > + const char *dirpath, > > + const char *filename, > > + QFileMonitorHandler cb, > > + void *opaque, > > + Error **errp) > > +{ > > + QFileMonitorDir *dir; > > + 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); > > + > > + 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; > > + > > + 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); > > + } > > + } > > + > > + dir->watches = g_renew(QFileMonitorWatch, dir->watches, dir->nwatches + 1); > > GArray could eventually make handling of watches a bit simpler > (counting, resizing, removing etc) ok, i'll have a look at that API. > > > + > > + dir->watches[dir->nwatches].id = ++dir->nextid; > > + dir->watches[dir->nwatches].filename = filename ? g_strdup(filename) : NULL; > > g_strdup(NULL) returns NULL already Ah, I forget that. > > > + dir->watches[dir->nwatches].cb = cb; > > + dir->watches[dir->nwatches].opaque = opaque; > > + dir->nwatches++; > > + > > + trace_qemu_file_monitor_add_watch(mon, dirpath, > > + filename ? filename : "<none>", > > + cb, opaque, > > + dir->watches[dir->nwatches - 1].id); > > + > > + ret = 0; > > + > > + 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->nwatches; i++) { > > + if (dir->watches[i].id == id) { > > + if (i < (dir->nwatches - 1)) { > > + memmove(dir->watches + i, > > + dir->watches + i + 1, > > + sizeof(QFileMonitorWatch) * > > + (dir->nwatches - (i + 1))); > > + dir->watches = g_renew(QFileMonitorWatch, dir->watches, > > + dir->nwatches - 1); > > + dir->nwatches--; > > + } > > + break; > > + } > > + } > > + > > + if (dir->nwatches == 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); > > + } > > + > > + cleanup: > > + qemu_mutex_lock(&mon->lock); > > +} > > + > > +#else > > +int > > +qemu_file_monitor_add_watch(QFileMonitor *mon, > > + const char *dirpath, > > + const char *filename, > > + QFileMonitorHandler cb, > > + void *opaque, > > + Error **errp) > > +{ > > + error_setg(errp, "File monitoring not available on this platform"); > > + return -1; > > +} > > + > > +void qemu_file_monitor_remove_watch(QFileMonitor *mon, > > + const char *dirpath, > > + int id) > > +{ > > +} > > Wouldn't it be cleaner with stubs/ ? I guess we could do it that way. > > Looks good, but would be even better with tests :) I'll see what I can do about that. Regards, Daniel
diff --git a/include/qemu/filemonitor.h b/include/qemu/filemonitor.h new file mode 100644 index 0000000000..1326272f0a --- /dev/null +++ b/include/qemu/filemonitor.h @@ -0,0 +1,117 @@ +/* + * QEMU file monitor helper + * + * 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 <http://www.gnu.org/licenses/>. + * + */ + +#ifndef QEMU_FILE_MONITOR_H +#define QEMU_FILE_MONITOR_H + +#include "qemu-common.h" + + +typedef struct QFileMonitor QFileMonitor; + +typedef enum { + /* File has been created in a dir */ + QFILE_MONITOR_EVENT_CREATED, + /* File has been modified in a dir */ + QFILE_MONITOR_EVENT_MODIFIED, + /* File has been deleted in a dir */ + QFILE_MONITOR_EVENT_DELETED, + /* Dir is no longer being monitored (due to deletion) */ + QFILE_MONITOR_EVENT_IGNORED, +} QFileMonitorEvent; + + +/** + * QFileMonitorHandler: + * @id: id from qemu_file_monitor_add_watch() + * @event: the file change that occurred + * @filename: the name of the file affected + * @opaque: opaque data provided to qemu_file_monitor_add_watch() + * + * Invoked whenever a file changes. If @event is + * QFILE_MONITOR_EVENT_IGNORED, @filename will be + * empty. + * + */ +typedef void (*QFileMonitorHandler)(int id, + QFileMonitorEvent event, + const char *filename, + void *opaque); + +/** + * qemu_file_monitor_get_instance: + * @errp: pointer to a NULL-initialized error object + * + * Acquire a handle to the shared file monitoring object. + * + * This object does locking internally to enable it to be + * safe to use from multiple threads + * + * If the platform does not support file monitoring, an + * error will be reported. Likewise if file monitoring + * is supported, but cannot be initialized + * + * Currently this is implemented on Linux platforms with + * the inotify subsystem. + * + * Returns: the shared monitoring object, or NULL on error + */ +QFileMonitor *qemu_file_monitor_get_instance(Error **errp); + +/** + * qemu_file_monitor_add_watch: + * @mon: the file monitor context + * @dirpath: the directory whose contents to watch + * @filename: optional filename to filter on + * @cb: the function to invoke when @dirpath has changes + * @opaque: data to pass to @cb + * @errp: pointer to a NULL-initialized error object + * + * Register to receive notifications of changes + * in the directory @dirpath. All files in the + * directory will be monitored. If the caller is + * only interested in one specific file, @filename + * can be used to filter events. + * + * Returns: a positive integer watch ID, or -1 on error + */ +int qemu_file_monitor_add_watch(QFileMonitor *mon, + const char *dirpath, + const char *filename, + QFileMonitorHandler cb, + void *opaque, + Error **errp); + +/** + * qemu_file_monitor_remove_watch: + * @mon: the file monitor context + * @dirpath: the directory whose contents to unwatch + * @id: id of the watch to remove + * + * Removes the file monitoring watch @id, associated + * with the directory @dirpath. This must never be + * called from a QFileMonitorHandler callback, or a + * deadlock will result. + */ +void qemu_file_monitor_remove_watch(QFileMonitor *mon, + const char *dirpath, + int id); + +#endif /* QEMU_FILE_MONITOR_H */ diff --git a/util/filemonitor.c b/util/filemonitor.c new file mode 100644 index 0000000000..67d7aedbe0 --- /dev/null +++ b/util/filemonitor.c @@ -0,0 +1,315 @@ +/* + * QEMU file_monitor helper + * + * 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 <http://www.gnu.org/licenses/>. + * + */ + +#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" + +struct QFileMonitor { + QemuMutex lock; + int fd; + + 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 */ + gsize nwatches; + QFileMonitorWatch *watches; +} QFileMonitorDir; + + +#ifdef CONFIG_INOTIFY1 +#include <sys/inotify.h> + +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 = read(mon->fd, buf, sizeof(buf)); + + qemu_mutex_lock(&mon->lock); + + if (len < 0) { + if (errno != EAGAIN) { + error_report("Failure monitoring inotify FD, disabling events"); + 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); + 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_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->nwatches; i++) { + QFileMonitorWatch *watch = &dir->watches[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; + + g_free(dir->watches); + g_free(dir); +} + +#endif + +static QFileMonitor * +qemu_file_monitor_new(Error **errp) +{ +#ifdef CONFIG_INOTIFY1 + 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; +#else + error_setg(errp, "File monitoring not available on this platform"); + return NULL; +#endif +} + + +QFileMonitor *qemu_file_monitor_get_instance(Error **errp) +{ + static QFileMonitor *global; + + if (!global) { + global = qemu_file_monitor_new(errp); + } + + return global; +} + + +#ifdef CONFIG_INOTIFY1 +int +qemu_file_monitor_add_watch(QFileMonitor *mon, + const char *dirpath, + const char *filename, + QFileMonitorHandler cb, + void *opaque, + Error **errp) +{ + QFileMonitorDir *dir; + 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); + + 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; + + 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); + } + } + + dir->watches = g_renew(QFileMonitorWatch, dir->watches, dir->nwatches + 1); + + dir->watches[dir->nwatches].id = ++dir->nextid; + dir->watches[dir->nwatches].filename = filename ? g_strdup(filename) : NULL; + dir->watches[dir->nwatches].cb = cb; + dir->watches[dir->nwatches].opaque = opaque; + dir->nwatches++; + + trace_qemu_file_monitor_add_watch(mon, dirpath, + filename ? filename : "<none>", + cb, opaque, + dir->watches[dir->nwatches - 1].id); + + ret = 0; + + 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->nwatches; i++) { + if (dir->watches[i].id == id) { + if (i < (dir->nwatches - 1)) { + memmove(dir->watches + i, + dir->watches + i + 1, + sizeof(QFileMonitorWatch) * + (dir->nwatches - (i + 1))); + dir->watches = g_renew(QFileMonitorWatch, dir->watches, + dir->nwatches - 1); + dir->nwatches--; + } + break; + } + } + + if (dir->nwatches == 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); + } + + cleanup: + qemu_mutex_lock(&mon->lock); +} + +#else +int +qemu_file_monitor_add_watch(QFileMonitor *mon, + const char *dirpath, + const char *filename, + QFileMonitorHandler cb, + void *opaque, + Error **errp) +{ + error_setg(errp, "File monitoring not available on this platform"); + return -1; +} + +void qemu_file_monitor_remove_watch(QFileMonitor *mon, + const char *dirpath, + int id) +{ +} +#endif + diff --git a/MAINTAINERS b/MAINTAINERS index 40672c4eba..29bbcf8c25 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -1866,6 +1866,12 @@ F: include/qemu/sockets.h F: util/qemu-sockets.c F: qapi/sockets.json +File monitor +M: Daniel P. Berrange <berrange@redhat.com> +S: Odd fixes +F: util/filemonitor.c +F: include/qemu/filemonitor.h + Throttling infrastructure M: Alberto Garcia <berto@igalia.com> S: Supported diff --git a/util/Makefile.objs b/util/Makefile.objs index 0820923c18..4d7675d6e7 100644 --- a/util/Makefile.objs +++ b/util/Makefile.objs @@ -50,5 +50,6 @@ util-obj-y += range.o util-obj-y += stats64.o util-obj-y += systemd.o util-obj-y += iova-tree.o +util-obj-y += filemonitor.o util-obj-$(CONFIG_LINUX) += vfio-helpers.o util-obj-$(CONFIG_OPENGL) += drm.o diff --git a/util/trace-events b/util/trace-events index 79569b7fdf..ff19b253e2 100644 --- a/util/trace-events +++ b/util/trace-events @@ -21,6 +21,15 @@ buffer_move_empty(const char *buf, size_t len, const char *from) "%s: %zd bytes buffer_move(const char *buf, size_t len, const char *from) "%s: %zd bytes from %s" buffer_free(const char *buf, size_t len) "%s: capacity %zd" +# util/filemonitor.c +qemu_file_monitor_add_watch(void *mon, const char *dirpath, const char *filename, void *cb, void *opaque, int id) "File monitor %p add watch dir='%s' file='%s' cb=%p opaque=%p id=%u" +qemu_file_monitor_remove_watch(void *mon, const char *dirpath, int id) "File monitor %p remove watch dir='%s' id=%u" +qemu_file_monitor_new(void *mon, int fd) "File monitor %p created fd=%d" +qemu_file_monitor_enable_watch(void *mon, const char *dirpath, int id) "File monitor %p enable watch dir='%s' id=%u" +qemu_file_monitor_disable_watch(void *mon, const char *dirpath, int id) "Fle monitor %p disable watch dir='%s' id=%u" +qemu_file_monitor_event(void *mon, const char *dirpath, const char *filename, int mask, unsigned int id) "File monitor %p event dir='%s' file='%s' mask=0x%x id=%u" +qemu_file_monitor_dispatch(void *mon, const char *dirpath, const char *filename, int ev, void *cb, void *opaque, unsigned int id) "File monitor %p dispatch dir='%s' file='%s' ev=%d cb=%p opaque=%p id=%u" + # util/qemu-coroutine.c qemu_aio_coroutine_enter(void *ctx, void *from, void *to, void *opaque) "ctx %p from %p to %p opaque %p" qemu_coroutine_yield(void *from, void *to) "from %p to %p"
The inotify userspace API for reading events is quite horrible, so it is useful to wrap it in a more friendly API to avoid duplicating code across many users in QEMU. Wrapping it also allows introduction of a platform portability layer, so that we can add impls for non-Linux based equivalents in future. Signed-off-by: Daniel P. Berrangé <berrange@redhat.com> --- include/qemu/filemonitor.h | 117 ++++++++++++++ util/filemonitor.c | 315 +++++++++++++++++++++++++++++++++++++ MAINTAINERS | 6 + util/Makefile.objs | 1 + util/trace-events | 9 ++ 5 files changed, 448 insertions(+) create mode 100644 include/qemu/filemonitor.h create mode 100644 util/filemonitor.c