blob: b5f4b93f3ff0639b398ef2ad380ba5b423cc18cc [file] [log] [blame]
/*
* 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 {
int64_t id; /* watch ID */
char *filename; /* optional filter */
QFileMonitorHandler cb;
void *opaque;
} QFileMonitorWatch;
typedef struct {
char *path;
int inotify_id; /* inotify ID */
int next_file_id; /* file 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->inotify_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);
}
int64_t
qemu_file_monitor_add_watch(QFileMonitor *mon,
const char *dirpath,
const char *filename,
QFileMonitorHandler cb,
void *opaque,
Error **errp)
{
QFileMonitorDir *dir;
QFileMonitorWatch watch;
int64_t 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->inotify_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 = (((int64_t)dir->inotify_id) << 32) | dir->next_file_id++;
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,
int64_t 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->inotify_id);
trace_qemu_file_monitor_disable_watch(mon, dir->path, dir->inotify_id);
g_hash_table_remove(mon->idmap, GINT_TO_POINTER(dir->inotify_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);
}