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.
Reviewed-by: Marc-André Lureau <marcandre.lur...@redhat.com> Signed-off-by: Daniel P. Berrangé <berra...@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 | 339 +++++++++++++++++ util/filemonitor-stub.c | 59 +++ util/trace-events | 9 + 9 files changed, 1234 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 6ae55ff732..9a0bbb7972 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -2087,6 +2087,13 @@ F: include/qemu/sockets.h F: util/qemu-sockets.c F: qapi/sockets.json +File monitor +M: Daniel P. Berrange <berra...@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 <be...@igalia.com> S: Supported diff --git a/Makefile.objs b/Makefile.objs index 5fb022d7ad..0897a346db 100644 --- a/Makefile.objs +++ b/Makefile.objs @@ -1,6 +1,6 @@ ####################################################################### # Common libraries for tools and emulators -stub-obj-y = stubs/ crypto/ +stub-obj-y = stubs/ util/ crypto/ util-obj-y = util/ qobject/ qapi/ chardev-obj-y = chardev/ 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..3a72be037f --- /dev/null +++ b/util/filemonitor-inotify.c @@ -0,0 +1,339 @@ +/* + * 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