90e33dfec6
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.lureau@redhat.com> Signed-off-by: Daniel P. Berrangé <berrange@redhat.com>
340 lines
9.1 KiB
C
340 lines
9.1 KiB
C
/*
|
|
* 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);
|
|
}
|