[libvirt] [PATCH v4 01/17] util: add helper APIs for dealing with inotify in portable manner

Marc-André Lureau marcandre.lureau at redhat.com
Sat Feb 16 11:27:48 UTC 2019


Hi

On Fri, Feb 15, 2019 at 6:13 PM Daniel P. Berrangé <berrange at 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 at redhat.com>

Reviewed-by: Marc-André Lureau <marcandre.lureau at redhat.com>

> ---
>  MAINTAINERS                   |   7 +
>  Makefile.objs                 |   2 +-
>  include/qemu/filemonitor.h    | 128 +++++++
>  tests/Makefile.include        |   3 +
>  tests/test-util-filemonitor.c | 685 ++++++++++++++++++++++++++++++++++
>  util/Makefile.objs            |   3 +
>  util/filemonitor-inotify.c    | 338 +++++++++++++++++
>  util/filemonitor-stub.c       |  59 +++
>  util/trace-events             |   9 +
>  9 files changed, 1233 insertions(+), 1 deletion(-)
>  create mode 100644 include/qemu/filemonitor.h
>  create mode 100644 tests/test-util-filemonitor.c
>  create mode 100644 util/filemonitor-inotify.c
>  create mode 100644 util/filemonitor-stub.c
>
> diff --git a/MAINTAINERS b/MAINTAINERS
> index ffb029f63a..5989796fa9 100644
> --- a/MAINTAINERS
> +++ b/MAINTAINERS
> @@ -2080,6 +2080,13 @@ F: include/qemu/sockets.h
>  F: util/qemu-sockets.c
>  F: qapi/sockets.json
>
> +File monitor
> +M: Daniel P. Berrange <berrange at redhat.com>
> +S: Odd fixes
> +F: util/filemonitor*.c
> +F: include/qemu/filemonitor.h
> +F: tests/test-util-filemonitor.c
> +
>  Throttling infrastructure
>  M: Alberto Garcia <berto at igalia.com>
>  S: Supported
> diff --git a/Makefile.objs b/Makefile.objs
> index b7aae33367..fee0ce9fc5 100644
> --- a/Makefile.objs
> +++ b/Makefile.objs
> @@ -4,7 +4,7 @@ QAPI_MODULES += ui
>
>  #######################################################################
>  # Common libraries for tools and emulators
> -stub-obj-y = stubs/ crypto/
> +stub-obj-y = stubs/ util/ crypto/
>  util-obj-y = util/ qobject/ qapi/
>  util-obj-y += qapi/qapi-builtin-types.o
>  util-obj-y += qapi/qapi-types.o
> diff --git a/include/qemu/filemonitor.h b/include/qemu/filemonitor.h
> new file mode 100644
> index 0000000000..cd031832ed
> --- /dev/null
> +++ b/include/qemu/filemonitor.h
> @@ -0,0 +1,128 @@
> +/*
> + * 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,
> +    /* File has attributes changed */
> +    QFILE_MONITOR_EVENT_ATTRIBUTES,
> +    /* 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_new:
> + * @errp: pointer to a NULL-initialized error object
> + *
> + * Create a handle for a 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 new monitoring object, or NULL on error
> + */
> +QFileMonitor *qemu_file_monitor_new(Error **errp);
> +
> +/**
> + * qemu_file_monitor_free:
> + * @mon: the file monitor context
> + *
> + * Free resources associated with the file monitor,
> + * including any currently registered watches.
> + */
> +void qemu_file_monitor_free(QFileMonitor *mon);
> +
> +/**
> + * 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/tests/Makefile.include b/tests/Makefile.include
> index b39e989f72..3a6a8d4b17 100644
> --- a/tests/Makefile.include
> +++ b/tests/Makefile.include
> @@ -114,6 +114,7 @@ ifneq (,$(findstring qemu-ga,$(TOOLS)))
>  check-unit-$(land,$(CONFIG_LINUX),$(CONFIG_VIRTIO_SERIAL)) += tests/test-qga$(EXESUF)
>  endif
>  check-unit-y += tests/test-timed-average$(EXESUF)
> +check-unit-$(CONFIG_INOTIFY1) += tests/test-util-filemonitor$(EXESUF)
>  check-unit-y += tests/test-util-sockets$(EXESUF)
>  check-unit-y += tests/test-io-task$(EXESUF)
>  check-unit-y += tests/test-io-channel-socket$(EXESUF)
> @@ -657,6 +658,8 @@ tests/test-crypto-tlssession$(EXESUF): tests/test-crypto-tlssession.o \
>         tests/crypto-tls-x509-helpers.o tests/pkix_asn1_tab.o \
>         tests/crypto-tls-psk-helpers.o \
>          $(test-crypto-obj-y)
> +tests/test-util-filemonitor$(EXESUF): tests/test-util-filemonitor.o \
> +       $(test-util-obj-y)
>  tests/test-util-sockets$(EXESUF): tests/test-util-sockets.o \
>         tests/socket-helpers.o $(test-util-obj-y)
>  tests/test-io-task$(EXESUF): tests/test-io-task.o $(test-io-obj-y)
> diff --git a/tests/test-util-filemonitor.c b/tests/test-util-filemonitor.c
> new file mode 100644
> index 0000000000..5d95cea5ee
> --- /dev/null
> +++ b/tests/test-util-filemonitor.c
> @@ -0,0 +1,685 @@
> +/*
> + * Tests for util/filemonitor-*.c
> + *
> + * Copyright 2018 Red Hat, Inc.
> + *
> + * 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 2 of the License, 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 library; if not, see <http://www.gnu.org/licenses/>.
> + *
> + */
> +
> +#include "qemu/osdep.h"
> +#include "qemu/main-loop.h"
> +#include "qapi/error.h"
> +#include "qemu/filemonitor.h"
> +
> +#include <utime.h>
> +
> +enum {
> +    QFILE_MONITOR_TEST_OP_CREATE,
> +    QFILE_MONITOR_TEST_OP_APPEND,
> +    QFILE_MONITOR_TEST_OP_TRUNC,
> +    QFILE_MONITOR_TEST_OP_RENAME,
> +    QFILE_MONITOR_TEST_OP_TOUCH,
> +    QFILE_MONITOR_TEST_OP_UNLINK,
> +};
> +
> +typedef struct {
> +    int type;
> +    const char *filesrc;
> +    const char *filedst;
> +} QFileMonitorTestOp;
> +
> +typedef struct {
> +    const char *file;
> +} QFileMonitorTestWatch;
> +
> +typedef struct {
> +    gsize nwatches;
> +    const QFileMonitorTestWatch *watches;
> +
> +    gsize nops;
> +    const QFileMonitorTestOp *ops;
> +} QFileMonitorTestPlan;
> +
> +typedef struct {
> +    int id;
> +    QFileMonitorEvent event;
> +    char *filename;
> +} QFileMonitorTestRecord;
> +
> +
> +typedef struct {
> +    QemuMutex lock;
> +    GList *records;
> +} QFileMonitorTestData;
> +
> +static QemuMutex evlock;
> +static bool evstopping;
> +static bool evrunning;
> +
> +/*
> + * Main function for a background thread that is
> + * running the event loop during the test
> + */
> +static void *
> +qemu_file_monitor_test_event_loop(void *opaque G_GNUC_UNUSED)
> +{
> +    qemu_mutex_lock(&evlock);
> +
> +    while (!evstopping) {
> +        qemu_mutex_unlock(&evlock);
> +        main_loop_wait(true);
> +        qemu_mutex_lock(&evlock);
> +    }
> +
> +    evrunning = false;
> +    qemu_mutex_unlock(&evlock);
> +    return NULL;
> +}
> +
> +
> +/*
> + * File monitor event handler which simply maintains
> + * an ordered list of all events that it receives
> + */
> +static void
> +qemu_file_monitor_test_handler(int id,
> +                               QFileMonitorEvent event,
> +                               const char *filename,
> +                               void *opaque)
> +{
> +    QFileMonitorTestData *data = opaque;
> +    QFileMonitorTestRecord *rec = g_new0(QFileMonitorTestRecord, 1);
> +
> +    rec->id = id;
> +    rec->event = event;
> +    rec->filename = g_strdup(filename);
> +
> +    qemu_mutex_lock(&data->lock);
> +    data->records = g_list_append(data->records, rec);
> +    qemu_mutex_unlock(&data->lock);
> +}
> +
> +
> +static void
> +qemu_file_monitor_test_record_free(QFileMonitorTestRecord *rec)
> +{
> +    g_free(rec->filename);
> +    g_free(rec);
> +}
> +
> +
> +/*
> + * Get the next event record that has been received by
> + * the file monitor event handler. Since events are
> + * emitted in the background thread running the event
> + * loop, we can't assume there is a record available
> + * immediately. Thus we will sleep for upto 5 seconds
> + * to wait for the event to be queued for us.
> + */
> +static QFileMonitorTestRecord *
> +qemu_file_monitor_test_next_record(QFileMonitorTestData *data)
> +{
> +    GTimer *timer = g_timer_new();
> +    QFileMonitorTestRecord *record = NULL;
> +    GList *tmp;
> +
> +    qemu_mutex_lock(&data->lock);
> +    while (!data->records && g_timer_elapsed(timer, NULL) < 5) {
> +        qemu_mutex_unlock(&data->lock);
> +        usleep(10 * 1000);
> +        qemu_mutex_lock(&data->lock);
> +    }
> +    if (data->records) {
> +        record = data->records->data;
> +        tmp = data->records;
> +        data->records = g_list_remove_link(data->records, tmp);
> +        g_list_free(tmp);
> +    }
> +    qemu_mutex_unlock(&data->lock);
> +
> +    g_timer_destroy(timer);
> +    return record;
> +}
> +
> +
> +/*
> + * Check whether the event record we retrieved matches
> + * data we were expecting to see for the event
> + */
> +static bool
> +qemu_file_monitor_test_expect(QFileMonitorTestData *data,
> +                              int id,
> +                              QFileMonitorEvent event,
> +                              const char *filename)
> +{
> +    QFileMonitorTestRecord *rec;
> +    bool ret = false;
> +
> +    rec = qemu_file_monitor_test_next_record(data);
> +
> +    if (!rec) {
> +        g_printerr("Missing event watch id %d event %d file %s\n",
> +                   id, event, filename);
> +        return false;
> +    }
> +
> +    if (id != rec->id) {
> +        g_printerr("Expected watch id %d but got %d\n", id, rec->id);
> +        goto cleanup;
> +    }
> +
> +    if (event != rec->event) {
> +        g_printerr("Expected event %d but got %d\n", event, rec->event);
> +        goto cleanup;
> +    }
> +
> +    if (!g_str_equal(filename, rec->filename)) {
> +        g_printerr("Expected filename %s but got %s\n",
> +                   filename, rec->filename);
> +        goto cleanup;
> +    }
> +
> +    ret = true;
> +
> + cleanup:
> +    qemu_file_monitor_test_record_free(rec);
> +    return ret;
> +}
> +
> +
> +static void
> +test_file_monitor_events(const void *opaque)
> +{
> +    const QFileMonitorTestPlan *plan = opaque;
> +    Error *local_err = NULL;
> +    GError *gerr = NULL;
> +    QFileMonitor *mon = qemu_file_monitor_new(&local_err);
> +    QemuThread th;
> +    GTimer *timer;
> +    gchar *dir = NULL;
> +    int err = -1;
> +    gsize i, j;
> +    char *pathsrc = NULL;
> +    char *pathdst = NULL;
> +    QFileMonitorTestData data;
> +
> +    qemu_mutex_init(&data.lock);
> +    data.records = NULL;
> +
> +    /*
> +     * The file monitor needs the main loop running in
> +     * order to receive events from inotify. We must
> +     * thus spawn a background thread to run an event
> +     * loop impl, while this thread triggers the
> +     * actual file operations we're testing
> +     */
> +    evrunning = 1;
> +    evstopping = 0;
> +    qemu_thread_create(&th, "event-loop",
> +                       qemu_file_monitor_test_event_loop, NULL,
> +                       QEMU_THREAD_JOINABLE);
> +
> +    if (local_err) {
> +        g_printerr("File monitoring not available: %s",
> +                   error_get_pretty(local_err));
> +        error_free(local_err);
> +        return;
> +    }
> +
> +    dir = g_dir_make_tmp("test-util-filemonitor-XXXXXX",
> +                         &gerr);
> +    if (!dir) {
> +        g_printerr("Unable to create tmp dir %s",
> +                   gerr->message);
> +        g_error_free(gerr);
> +        abort();
> +    }
> +
> +    /*
> +     * First register all the directory / file watches
> +     * we're interested in seeing events against
> +     */
> +    for (i = 0; i < plan->nwatches; i++) {
> +        int watchid;
> +        watchid = qemu_file_monitor_add_watch(mon,
> +                                              dir,
> +                                              plan->watches[i].file,
> +                                              qemu_file_monitor_test_handler,
> +                                              &data,
> +                                              &local_err);
> +        if (watchid < 0) {
> +            g_printerr("Unable to add watch %s",
> +                       error_get_pretty(local_err));
> +            goto cleanup;
> +        }
> +    }
> +
> +
> +    /*
> +     * Now invoke all the file operations (create,
> +     * delete, rename, chmod, etc). These operations
> +     * will trigger the various file monitor events
> +     */
> +    for (i = 0; i < plan->nops; i++) {
> +        const QFileMonitorTestOp *op = &(plan->ops[i]);
> +        int fd;
> +        struct utimbuf ubuf;
> +
> +        pathsrc = g_strdup_printf("%s/%s", dir, op->filesrc);
> +        if (op->filedst) {
> +            pathdst = g_strdup_printf("%s/%s", dir, op->filedst);
> +        }
> +
> +        switch (op->type) {
> +        case QFILE_MONITOR_TEST_OP_CREATE:
> +            fd = open(pathsrc, O_WRONLY | O_CREAT, 0700);
> +            if (fd < 0) {
> +                g_printerr("Unable to create %s: %s",
> +                           pathsrc, strerror(errno));
> +                goto cleanup;
> +            }
> +            close(fd);
> +            break;
> +
> +        case QFILE_MONITOR_TEST_OP_APPEND:
> +            fd = open(pathsrc, O_WRONLY | O_APPEND, 0700);
> +            if (fd < 0) {
> +                g_printerr("Unable to open %s: %s",
> +                           pathsrc, strerror(errno));
> +                goto cleanup;
> +            }
> +
> +            if (write(fd, "Hello World", 10) != 10) {
> +                g_printerr("Unable to write %s: %s",
> +                           pathsrc, strerror(errno));
> +                close(fd);
> +                goto cleanup;
> +            }
> +            close(fd);
> +            break;
> +
> +        case QFILE_MONITOR_TEST_OP_TRUNC:
> +            if (truncate(pathsrc, 4) < 0) {
> +                g_printerr("Unable to truncate %s: %s",
> +                           pathsrc, strerror(errno));
> +                goto cleanup;
> +            }
> +            break;
> +
> +        case QFILE_MONITOR_TEST_OP_RENAME:
> +            if (rename(pathsrc, pathdst) < 0) {
> +                g_printerr("Unable to rename %s to %s: %s",
> +                           pathsrc, pathdst, strerror(errno));
> +                goto cleanup;
> +            }
> +            break;
> +
> +        case QFILE_MONITOR_TEST_OP_UNLINK:
> +            if (unlink(pathsrc) < 0) {
> +                g_printerr("Unable to unlink %s: %s",
> +                           pathsrc, strerror(errno));
> +                goto cleanup;
> +            }
> +            break;
> +
> +        case QFILE_MONITOR_TEST_OP_TOUCH:
> +            ubuf.actime = 1024;
> +            ubuf.modtime = 1025;
> +            if (utime(pathsrc, &ubuf) < 0) {
> +                g_printerr("Unable to touch %s: %s",
> +                           pathsrc, strerror(errno));
> +                goto cleanup;
> +            }
> +            break;
> +
> +        default:
> +            g_assert_not_reached();
> +        }
> +
> +        g_free(pathsrc);
> +        g_free(pathdst);
> +        pathsrc = pathdst = NULL;
> +    }
> +
> +
> +    /*
> +     * Finally validate that we have received all the events
> +     * we expect to see for the combination of watches and
> +     * file operations
> +     */
> +    for (i = 0; i < plan->nops; i++) {
> +        const QFileMonitorTestOp *op = &(plan->ops[i]);
> +
> +        switch (op->type) {
> +        case QFILE_MONITOR_TEST_OP_CREATE:
> +            for (j = 0; j < plan->nwatches; j++) {
> +                if (plan->watches[j].file &&
> +                    !g_str_equal(plan->watches[j].file, op->filesrc))
> +                    continue;
> +
> +                if (!qemu_file_monitor_test_expect(
> +                        &data, j, QFILE_MONITOR_EVENT_CREATED, op->filesrc))
> +                    goto cleanup;
> +            }
> +            break;
> +
> +        case QFILE_MONITOR_TEST_OP_APPEND:
> +        case QFILE_MONITOR_TEST_OP_TRUNC:
> +            for (j = 0; j < plan->nwatches; j++) {
> +                if (plan->watches[j].file &&
> +                    !g_str_equal(plan->watches[j].file, op->filesrc))
> +                    continue;
> +
> +                if (!qemu_file_monitor_test_expect(
> +                        &data, j, QFILE_MONITOR_EVENT_MODIFIED, op->filesrc))
> +                    goto cleanup;
> +            }
> +            break;
> +
> +        case QFILE_MONITOR_TEST_OP_RENAME:
> +            for (j = 0; j < plan->nwatches; j++) {
> +                if (plan->watches[j].file &&
> +                    !g_str_equal(plan->watches[j].file, op->filesrc))
> +                    continue;
> +
> +                if (!qemu_file_monitor_test_expect(
> +                        &data, j, QFILE_MONITOR_EVENT_DELETED, op->filesrc))
> +                    goto cleanup;
> +            }
> +
> +            for (j = 0; j < plan->nwatches; j++) {
> +                if (plan->watches[j].file &&
> +                    !g_str_equal(plan->watches[j].file, op->filedst))
> +                    continue;
> +
> +                if (!qemu_file_monitor_test_expect(
> +                        &data, j, QFILE_MONITOR_EVENT_CREATED, op->filedst))
> +                    goto cleanup;
> +            }
> +            break;
> +
> +        case QFILE_MONITOR_TEST_OP_TOUCH:
> +            for (j = 0; j < plan->nwatches; j++) {
> +                if (plan->watches[j].file &&
> +                    !g_str_equal(plan->watches[j].file, op->filesrc))
> +                    continue;
> +
> +                if (!qemu_file_monitor_test_expect(
> +                        &data, j, QFILE_MONITOR_EVENT_ATTRIBUTES, op->filesrc))
> +                    goto cleanup;
> +            }
> +            break;
> +
> +        case QFILE_MONITOR_TEST_OP_UNLINK:
> +            for (j = 0; j < plan->nwatches; j++) {
> +                if (plan->watches[j].file &&
> +                    !g_str_equal(plan->watches[j].file, op->filesrc))
> +                    continue;
> +
> +                if (!qemu_file_monitor_test_expect(
> +                        &data, j, QFILE_MONITOR_EVENT_DELETED, op->filesrc))
> +                    goto cleanup;
> +            }
> +            break;
> +
> +        default:
> +            g_assert_not_reached();
> +        }
> +    }
> +
> +    err = 0;
> +
> + cleanup:
> +    g_free(pathsrc);
> +    g_free(pathdst);
> +
> +    qemu_mutex_lock(&evlock);
> +    evstopping = 1;
> +    timer = g_timer_new();
> +    while (evrunning && g_timer_elapsed(timer, NULL) < 5) {
> +        qemu_mutex_unlock(&evlock);
> +        usleep(10 * 1000);
> +        qemu_mutex_lock(&evlock);
> +    }
> +    qemu_mutex_unlock(&evlock);
> +
> +    if (g_timer_elapsed(timer, NULL) >= 5) {
> +        g_printerr("Event loop failed to quit after 5 seconds\n");
> +    }
> +    g_timer_destroy(timer);
> +
> +    for (i = 0; i < plan->nops; i++) {
> +        const QFileMonitorTestOp *op = &(plan->ops[i]);
> +        pathsrc = g_strdup_printf("%s/%s", dir, op->filesrc);
> +        unlink(pathsrc);
> +        g_free(pathsrc);
> +        if (op->filedst) {
> +            pathdst = g_strdup_printf("%s/%s", dir, op->filedst);
> +            unlink(pathdst);
> +            g_free(pathdst);
> +        }
> +    }
> +
> +    qemu_file_monitor_free(mon);
> +    g_list_foreach(data.records,
> +                   (GFunc)qemu_file_monitor_test_record_free, NULL);
> +    g_list_free(data.records);
> +    qemu_mutex_destroy(&data.lock);
> +    if (dir) {
> +        rmdir(dir);
> +    }
> +    g_free(dir);
> +    g_assert(err == 0);
> +}
> +
> +
> +/*
> + * Set of structs which define which file name patterns
> + * we're trying to watch against. NULL, means all files
> + * in the directory
> + */
> +static const QFileMonitorTestWatch watches_any[] = {
> +    { NULL },
> +};
> +
> +static const QFileMonitorTestWatch watches_one[] = {
> +    { "one.txt" },
> +};
> +
> +static const QFileMonitorTestWatch watches_two[] = {
> +    { "two.txt" },
> +};
> +
> +static const QFileMonitorTestWatch watches_many[] = {
> +    { NULL },
> +    { "one.txt" },
> +    { "two.txt" },
> +};
> +
> +
> +/*
> + * Various sets of file operations we're going to
> + * trigger and validate events for
> + */
> +static const QFileMonitorTestOp ops_create_one[] = {
> +    { .type = QFILE_MONITOR_TEST_OP_CREATE,
> +      .filesrc = "one.txt", }
> +};
> +
> +static const QFileMonitorTestOp ops_delete_one[] = {
> +    { .type = QFILE_MONITOR_TEST_OP_CREATE,
> +      .filesrc = "one.txt", },
> +    { .type = QFILE_MONITOR_TEST_OP_UNLINK,
> +      .filesrc = "one.txt", }
> +};
> +
> +static const QFileMonitorTestOp ops_create_many[] = {
> +    { .type = QFILE_MONITOR_TEST_OP_CREATE,
> +      .filesrc = "one.txt", },
> +    { .type = QFILE_MONITOR_TEST_OP_CREATE,
> +      .filesrc = "two.txt", },
> +    { .type = QFILE_MONITOR_TEST_OP_CREATE,
> +      .filesrc = "three.txt", }
> +};
> +
> +static const QFileMonitorTestOp ops_rename_one[] = {
> +    { .type = QFILE_MONITOR_TEST_OP_CREATE,
> +      .filesrc = "one.txt", },
> +    { .type = QFILE_MONITOR_TEST_OP_RENAME,
> +      .filesrc = "one.txt", .filedst = "two.txt" }
> +};
> +
> +static const QFileMonitorTestOp ops_rename_many[] = {
> +    { .type = QFILE_MONITOR_TEST_OP_CREATE,
> +      .filesrc = "one.txt", },
> +    { .type = QFILE_MONITOR_TEST_OP_CREATE,
> +      .filesrc = "two.txt", },
> +    { .type = QFILE_MONITOR_TEST_OP_RENAME,
> +      .filesrc = "one.txt", .filedst = "two.txt" }
> +};
> +
> +static const QFileMonitorTestOp ops_append_one[] = {
> +    { .type = QFILE_MONITOR_TEST_OP_CREATE,
> +      .filesrc = "one.txt", },
> +    { .type = QFILE_MONITOR_TEST_OP_APPEND,
> +      .filesrc = "one.txt", },
> +};
> +
> +static const QFileMonitorTestOp ops_trunc_one[] = {
> +    { .type = QFILE_MONITOR_TEST_OP_CREATE,
> +      .filesrc = "one.txt", },
> +    { .type = QFILE_MONITOR_TEST_OP_TRUNC,
> +      .filesrc = "one.txt", },
> +};
> +
> +static const QFileMonitorTestOp ops_touch_one[] = {
> +    { .type = QFILE_MONITOR_TEST_OP_CREATE,
> +      .filesrc = "one.txt", },
> +    { .type = QFILE_MONITOR_TEST_OP_TOUCH,
> +      .filesrc = "one.txt", },
> +};
> +
> +
> +/*
> + * No we define data sets for the combinatorial
> + * expansion of file watches and operation sets
> + */
> +#define PLAN_DATA(o, w) \
> +    static const QFileMonitorTestPlan plan_ ## o ## _ ## w = { \
> +        .nops = G_N_ELEMENTS(ops_ ##o), \
> +        .ops = ops_ ##o, \
> +        .nwatches = G_N_ELEMENTS(watches_ ##w), \
> +        .watches = watches_ ## w, \
> +    }
> +
> +PLAN_DATA(create_one, any);
> +PLAN_DATA(create_one, one);
> +PLAN_DATA(create_one, two);
> +PLAN_DATA(create_one, many);
> +
> +PLAN_DATA(delete_one, any);
> +PLAN_DATA(delete_one, one);
> +PLAN_DATA(delete_one, two);
> +PLAN_DATA(delete_one, many);
> +
> +PLAN_DATA(create_many, any);
> +PLAN_DATA(create_many, one);
> +PLAN_DATA(create_many, two);
> +PLAN_DATA(create_many, many);
> +
> +PLAN_DATA(rename_one, any);
> +PLAN_DATA(rename_one, one);
> +PLAN_DATA(rename_one, two);
> +PLAN_DATA(rename_one, many);
> +
> +PLAN_DATA(rename_many, any);
> +PLAN_DATA(rename_many, one);
> +PLAN_DATA(rename_many, two);
> +PLAN_DATA(rename_many, many);
> +
> +PLAN_DATA(append_one, any);
> +PLAN_DATA(append_one, one);
> +PLAN_DATA(append_one, two);
> +PLAN_DATA(append_one, many);
> +
> +PLAN_DATA(trunc_one, any);
> +PLAN_DATA(trunc_one, one);
> +PLAN_DATA(trunc_one, two);
> +PLAN_DATA(trunc_one, many);
> +
> +PLAN_DATA(touch_one, any);
> +PLAN_DATA(touch_one, one);
> +PLAN_DATA(touch_one, two);
> +PLAN_DATA(touch_one, many);
> +
> +
> +int main(int argc, char **argv)
> +{
> +    g_test_init(&argc, &argv, NULL);
> +
> +    qemu_init_main_loop(&error_abort);
> +
> +    qemu_mutex_init(&evlock);
> +
> +    /*
> +     * Register test cases for the combinatorial
> +     * expansion of file watches and operation sets
> +     */
> +    #define PLAN_REGISTER(o, w)                                         \
> +        g_test_add_data_func("/util/filemonitor/" # o "/" # w,          \
> +                             &plan_ ## o ## _ ## w, test_file_monitor_events)
> +
> +    PLAN_REGISTER(create_one, any);
> +    PLAN_REGISTER(create_one, one);
> +    PLAN_REGISTER(create_one, two);
> +    PLAN_REGISTER(create_one, many);
> +
> +    PLAN_REGISTER(delete_one, any);
> +    PLAN_REGISTER(delete_one, one);
> +    PLAN_REGISTER(delete_one, two);
> +    PLAN_REGISTER(delete_one, many);
> +
> +    PLAN_REGISTER(create_many, any);
> +    PLAN_REGISTER(create_many, one);
> +    PLAN_REGISTER(create_many, two);
> +    PLAN_REGISTER(create_many, many);
> +
> +    PLAN_REGISTER(rename_one, any);
> +    PLAN_REGISTER(rename_one, one);
> +    PLAN_REGISTER(rename_one, two);
> +    PLAN_REGISTER(rename_one, many);
> +
> +    PLAN_REGISTER(rename_many, any);
> +    PLAN_REGISTER(rename_many, one);
> +    PLAN_REGISTER(rename_many, two);
> +    PLAN_REGISTER(rename_many, many);
> +
> +    PLAN_REGISTER(append_one, any);
> +    PLAN_REGISTER(append_one, one);
> +    PLAN_REGISTER(append_one, two);
> +    PLAN_REGISTER(append_one, many);
> +
> +    PLAN_REGISTER(trunc_one, any);
> +    PLAN_REGISTER(trunc_one, one);
> +    PLAN_REGISTER(trunc_one, two);
> +    PLAN_REGISTER(trunc_one, many);
> +
> +    PLAN_REGISTER(touch_one, any);
> +    PLAN_REGISTER(touch_one, one);
> +    PLAN_REGISTER(touch_one, two);
> +    PLAN_REGISTER(touch_one, many);
> +
> +    return g_test_run();
> +}
> diff --git a/util/Makefile.objs b/util/Makefile.objs
> index 0820923c18..c1dd746902 100644
> --- a/util/Makefile.objs
> +++ b/util/Makefile.objs
> @@ -50,5 +50,8 @@ util-obj-y += range.o
>  util-obj-y += stats64.o
>  util-obj-y += systemd.o
>  util-obj-y += iova-tree.o
> +util-obj-$(CONFIG_INOTIFY1) += filemonitor-inotify.o
>  util-obj-$(CONFIG_LINUX) += vfio-helpers.o
>  util-obj-$(CONFIG_OPENGL) += drm.o
> +
> +stub-obj-y += filemonitor-stub.o
> diff --git a/util/filemonitor-inotify.c b/util/filemonitor-inotify.c
> new file mode 100644
> index 0000000000..c5e0a6fee0
> --- /dev/null
> +++ b/util/filemonitor-inotify.c
> @@ -0,0 +1,338 @@
> +/*
> + * 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 <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"
> +
> +#include <sys/inotify.h>
> +
> +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 : "<none>",
> +                                      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);
> +}
> diff --git a/util/filemonitor-stub.c b/util/filemonitor-stub.c
> new file mode 100644
> index 0000000000..48268b2bb6
> --- /dev/null
> +++ b/util/filemonitor-stub.c
> @@ -0,0 +1,59 @@
> +/*
> + * QEMU file monitor stub 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 <http://www.gnu.org/licenses/>.
> + *
> + */
> +
> +#include "qemu/osdep.h"
> +#include "qemu/filemonitor.h"
> +#include "qemu/error-report.h"
> +#include "qapi/error.h"
> +
> +
> +QFileMonitor *
> +qemu_file_monitor_new(Error **errp)
> +{
> +    error_setg(errp, "File monitoring not available on this platform");
> +    return NULL;
> +}
> +
> +
> +void
> +qemu_file_monitor_free(QFileMonitor *mon G_GNUC_UNUSED)
> +{
> +}
> +
> +
> +int
> +qemu_file_monitor_add_watch(QFileMonitor *mon G_GNUC_UNUSED,
> +                            const char *dirpath G_GNUC_UNUSED,
> +                            const char *filename G_GNUC_UNUSED,
> +                            QFileMonitorHandler cb G_GNUC_UNUSED,
> +                            void *opaque G_GNUC_UNUSED,
> +                            Error **errp)
> +{
> +    error_setg(errp, "File monitoring not available on this platform");
> +    return -1;
> +}
> +
> +
> +void
> +qemu_file_monitor_remove_watch(QFileMonitor *mon G_GNUC_UNUSED,
> +                               const char *dirpath G_GNUC_UNUSED,
> +                               int id G_GNUC_UNUSED)
> +{
> +}
> 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.20.1
>




More information about the libvir-list mailing list