diff mbox series

[v6,01/11] util: add helper APIs for dealing with inotify in portable manner

Message ID 20181019133835.16494-2-berrange@redhat.com
State New
Headers show
Series Add a standard authorization framework | expand

Commit Message

Daniel P. Berrangé Oct. 19, 2018, 1:38 p.m. UTC
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

Comments

Marc-André Lureau Nov. 7, 2018, 6:08 p.m. UTC | #1
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 :)
Daniel P. Berrangé Nov. 12, 2018, 4:49 p.m. UTC | #2
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 mbox series

Patch

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"