blob: 61b95f6ca09164a7de0c3ff280b83d3ccf1862b4 [file] [log] [blame]
/*
* Copyright (C) 2014 Karel Zak <kzak@redhat.com>
*
* This file may be redistributed under the terms of the
* GNU Lesser General Public License.
*/
/**
* SECTION: monitor
* @title: Monitor
* @short_description: interface to monitor mount tables
*
* For example monitor VFS (/proc/self/mountinfo) for changes:
*
* <informalexample>
* <programlisting>
* const char *filename;
* struct libmount_monitor *mn = mnt_new_monitor();
*
* mnt_monitor_enable_kernel(mn, TRUE));
*
* printf("waiting for changes...\n");
* while (mnt_monitor_wait(mn, -1) > 0) {
* while (mnt_monitor_next_change(mn, &filename, NULL) == 0)
* printf(" %s: change detected\n", filename);
* }
* mnt_unref_monitor(mn);
* </programlisting>
* </informalexample>
*
*/
#include "fileutils.h"
#include "mountP.h"
#include "pathnames.h"
#include <sys/inotify.h>
#include <sys/epoll.h>
struct monitor_opers;
struct monitor_entry {
int fd; /* private entry file descriptor */
char *path; /* path to the monitored file */
int type; /* MNT_MONITOR_TYPE_* */
uint32_t events; /* wanted epoll events */
const struct monitor_opers *opers;
unsigned int enable : 1,
changed : 1;
struct list_head ents;
};
struct libmnt_monitor {
int refcount;
int fd; /* public monitor file descriptor */
struct list_head ents;
};
struct monitor_opers {
int (*op_get_fd)(struct libmnt_monitor *, struct monitor_entry *);
int (*op_close_fd)(struct libmnt_monitor *, struct monitor_entry *);
int (*op_event_verify)(struct libmnt_monitor *, struct monitor_entry *);
};
static int monitor_modify_epoll(struct libmnt_monitor *mn,
struct monitor_entry *me, int enable);
/**
* mnt_new_monitor:
*
* The initial refcount is 1, and needs to be decremented to
* release the resources of the filesystem.
*
* Returns: newly allocated struct libmnt_monitor.
*/
struct libmnt_monitor *mnt_new_monitor(void)
{
struct libmnt_monitor *mn = calloc(1, sizeof(*mn));
if (!mn)
return NULL;
mn->refcount = 1;
mn->fd = -1;
INIT_LIST_HEAD(&mn->ents);
DBG(MONITOR, ul_debugobj(mn, "alloc"));
return mn;
}
/**
* mnt_ref_monitor:
* @mn: monitor pointer
*
* Increments reference counter.
*/
void mnt_ref_monitor(struct libmnt_monitor *mn)
{
if (mn)
mn->refcount++;
}
static void free_monitor_entry(struct monitor_entry *me)
{
if (!me)
return;
list_del(&me->ents);
if (me->fd >= 0)
close(me->fd);
free(me->path);
free(me);
}
/**
* mnt_unref_monitor:
* @mn: monitor pointer
*
* Decrements the reference counter, on zero the @mn is automatically
* deallocated.
*/
void mnt_unref_monitor(struct libmnt_monitor *mn)
{
if (!mn)
return;
mn->refcount--;
if (mn->refcount <= 0) {
mnt_monitor_close_fd(mn); /* destroys all file descriptors */
while (!list_empty(&mn->ents)) {
struct monitor_entry *me = list_entry(mn->ents.next,
struct monitor_entry, ents);
free_monitor_entry(me);
}
free(mn);
}
}
static struct monitor_entry *monitor_new_entry(struct libmnt_monitor *mn)
{
struct monitor_entry *me;
assert(mn);
me = calloc(1, sizeof(*me));
if (!me)
return NULL;
INIT_LIST_HEAD(&me->ents);
list_add_tail(&me->ents, &mn->ents);
me->fd = -1;
return me;
}
static int monitor_next_entry(struct libmnt_monitor *mn,
struct libmnt_iter *itr,
struct monitor_entry **me)
{
int rc = 1;
assert(mn);
assert(itr);
assert(me);
*me = NULL;
if (!itr->head)
MNT_ITER_INIT(itr, &mn->ents);
if (itr->p != itr->head) {
MNT_ITER_ITERATE(itr, *me, struct monitor_entry, ents);
rc = 0;
}
return rc;
}
/* returns entry by type */
static struct monitor_entry *monitor_get_entry(struct libmnt_monitor *mn, int type)
{
struct libmnt_iter itr;
struct monitor_entry *me;
mnt_reset_iter(&itr, MNT_ITER_FORWARD);
while (monitor_next_entry(mn, &itr, &me) == 0) {
if (me->type == type)
return me;
}
return NULL;
}
/*
* Userspace monitor
*/
static int userspace_monitor_close_fd(struct libmnt_monitor *mn __attribute__((__unused__)),
struct monitor_entry *me)
{
assert(me);
if (me->fd >= 0)
close(me->fd);
me->fd = -1;
return 0;
}
static int userspace_add_watch(struct monitor_entry *me, int *final, int *fd)
{
char *filename = NULL;
int wd, rc = -EINVAL;
assert(me);
assert(me->path);
/*
* libmount uses rename(2) to atomically update utab, monitor
* rename changes is too tricky. It seems better to monitor utab
* lockfile close.
*/
if (asprintf(&filename, "%s.lock", me->path) <= 0) {
rc = -errno;
goto done;
}
/* try lock file if already exists */
errno = 0;
wd = inotify_add_watch(me->fd, filename, IN_CLOSE_NOWRITE);
if (wd >= 0) {
DBG(MONITOR, ul_debug(" added inotify watch for %s [fd=%d]", filename, wd));
rc = 0;
if (final)
*final = 1;
if (fd)
*fd = wd;
goto done;
} else if (errno != ENOENT) {
rc = -errno;
goto done;
}
while (strchr(filename, '/')) {
stripoff_last_component(filename);
if (!*filename)
break;
/* try directory where is the lock file */
errno = 0;
wd = inotify_add_watch(me->fd, filename, IN_CREATE|IN_ISDIR);
if (wd >= 0) {
DBG(MONITOR, ul_debug(" added inotify watch for %s [fd=%d]", filename, wd));
rc = 0;
if (fd)
*fd = wd;
break;
} else if (errno != ENOENT) {
rc = -errno;
break;
}
}
done:
free(filename);
return rc;
}
static int userspace_monitor_get_fd(struct libmnt_monitor *mn,
struct monitor_entry *me)
{
int rc;
if (!me || me->enable == 0) /* not-initialized or disabled */
return -EINVAL;
if (me->fd >= 0)
return me->fd; /* already initialized */
assert(me->path);
DBG(MONITOR, ul_debugobj(mn, " open userspace monitor for %s", me->path));
me->fd = inotify_init1(IN_NONBLOCK | IN_CLOEXEC);
if (me->fd < 0)
goto err;
if (userspace_add_watch(me, NULL, NULL) < 0)
goto err;
return me->fd;
err:
rc = -errno;
if (me->fd >= 0)
close(me->fd);
me->fd = -1;
DBG(MONITOR, ul_debugobj(mn, "failed to create userspace monitor [rc=%d]", rc));
return rc;
}
/*
* verify and drain inotify buffer
*/
static int userspace_event_verify(struct libmnt_monitor *mn,
struct monitor_entry *me)
{
char buf[sizeof(struct inotify_event) + NAME_MAX + 1];
int status = 0;
if (!me || me->fd < 0)
return 0;
DBG(MONITOR, ul_debugobj(mn, "drain and verify userspace monitor inotify"));
/* the me->fd is non-blocking */
do {
ssize_t len;
char *p;
const struct inotify_event *e;
len = read(me->fd, buf, sizeof(buf));
if (len < 0)
break;
for (p = buf; p < buf + len;
p += sizeof(struct inotify_event) + e->len) {
int fd = -1;
e = (const struct inotify_event *) p;
DBG(MONITOR, ul_debugobj(mn, " inotify event 0x%x [%s]\n", e->mask, e->len ? e->name : ""));
if (e->mask & IN_CLOSE_NOWRITE)
status = 1;
else {
/* event on lock file */
userspace_add_watch(me, &status, &fd);
if (fd != e->wd) {
DBG(MONITOR, ul_debugobj(mn, " removing watch [fd=%d]", e->wd));
inotify_rm_watch(me->fd, e->wd);
}
}
}
} while (1);
DBG(MONITOR, ul_debugobj(mn, "%s", status == 1 ? " success" : " nothing"));
return status;
}
/*
* userspace monitor operations
*/
static const struct monitor_opers userspace_opers = {
.op_get_fd = userspace_monitor_get_fd,
.op_close_fd = userspace_monitor_close_fd,
.op_event_verify = userspace_event_verify
};
/**
* mnt_monitor_enable_userspace:
* @mn: monitor
* @enable: 0 or 1
* @filename: overwrites default
*
* Enables or disables userspace monitoring. If the userspace monitor does not
* exist and enable=1 then allocates new resources necessary for the monitor.
*
* If the top-level monitor has been already created (by mnt_monitor_get_fd()
* or mnt_monitor_wait()) then it's updated according to @enable.
*
* The @filename is used only the first time when you enable the monitor. It's
* impossible to have more than one userspace monitor. The recommended is to
* use NULL as filename.
*
* The userspace monitor is unsupported for systems with classic regular
* /etc/mtab file.
*
* Return: 0 on success and <0 on error
*/
int mnt_monitor_enable_userspace(struct libmnt_monitor *mn, int enable, const char *filename)
{
struct monitor_entry *me;
int rc = 0;
if (!mn)
return -EINVAL;
me = monitor_get_entry(mn, MNT_MONITOR_TYPE_USERSPACE);
if (me) {
rc = monitor_modify_epoll(mn, me, enable);
if (!enable)
userspace_monitor_close_fd(mn, me);
return rc;
}
if (!enable)
return 0;
DBG(MONITOR, ul_debugobj(mn, "allocate new userspace monitor"));
if (!filename)
filename = mnt_get_utab_path(); /* /run/mount/utab */
if (!filename) {
DBG(MONITOR, ul_debugobj(mn, "failed to get userspace mount table path"));
return -EINVAL;
}
me = monitor_new_entry(mn);
if (!me)
goto err;
me->type = MNT_MONITOR_TYPE_USERSPACE;
me->opers = &userspace_opers;
me->events = EPOLLIN;
me->path = strdup(filename);
if (!me->path)
goto err;
return monitor_modify_epoll(mn, me, TRUE);
err:
rc = -errno;
free_monitor_entry(me);
DBG(MONITOR, ul_debugobj(mn, "failed to allocate userspace monitor [rc=%d]", rc));
return rc;
}
/*
* Kernel monitor
*/
static int kernel_monitor_close_fd(struct libmnt_monitor *mn __attribute__((__unused__)),
struct monitor_entry *me)
{
assert(me);
if (me->fd >= 0)
close(me->fd);
me->fd = -1;
return 0;
}
static int kernel_monitor_get_fd(struct libmnt_monitor *mn,
struct monitor_entry *me)
{
int rc;
if (!me || me->enable == 0) /* not-initialized or disabled */
return -EINVAL;
if (me->fd >= 0)
return me->fd; /* already initialized */
assert(me->path);
DBG(MONITOR, ul_debugobj(mn, " open kernel monitor for %s", me->path));
me->fd = open(me->path, O_RDONLY|O_CLOEXEC);
if (me->fd < 0)
goto err;
return me->fd;
err:
rc = -errno;
DBG(MONITOR, ul_debugobj(mn, "failed to create kernel monitor [rc=%d]", rc));
return rc;
}
/*
* kernel monitor operations
*/
static const struct monitor_opers kernel_opers = {
.op_get_fd = kernel_monitor_get_fd,
.op_close_fd = kernel_monitor_close_fd,
};
/**
* mnt_monitor_enable_kernel:
* @mn: monitor
* @enable: 0 or 1
*
* Enables or disables kernel VFS monitoring. If the monitor does not exist and
* enable=1 then allocates new resources necessary for the monitor.
*
* If the top-level monitor has been already created (by mnt_monitor_get_fd()
* or mnt_monitor_wait()) then it's updated according to @enable.
*
* Return: 0 on success and <0 on error
*/
int mnt_monitor_enable_kernel(struct libmnt_monitor *mn, int enable)
{
struct monitor_entry *me;
int rc = 0;
if (!mn)
return -EINVAL;
me = monitor_get_entry(mn, MNT_MONITOR_TYPE_KERNEL);
if (me) {
rc = monitor_modify_epoll(mn, me, enable);
if (!enable)
kernel_monitor_close_fd(mn, me);
return rc;
}
if (!enable)
return 0;
DBG(MONITOR, ul_debugobj(mn, "allocate new kernel monitor"));
/* create a new entry */
me = monitor_new_entry(mn);
if (!me)
goto err;
/* If you want to use epoll FD in another epoll then top level
* epoll_wait() will drain all events from low-level FD if the
* low-level FD is not added with EPOLLIN. It means without EPOLLIN it
* it's impossible to detect which low-level FD has been active.
*
* Unfortunately, use EPOLLIN for mountinfo is tricky because in this
* case kernel returns events all time (we don't read from the FD).
* The solution is to use also edge-triggered (EPOLLET) flag, then
* kernel generate events on mountinfo changes only. The disadvantage is
* that we have to drain initial event generated by EPOLLIN after
* epoll_ctl(ADD). See monitor_modify_epoll().
*/
me->events = EPOLLIN | EPOLLET;
me->type = MNT_MONITOR_TYPE_KERNEL;
me->opers = &kernel_opers;
me->path = strdup(_PATH_PROC_MOUNTINFO);
if (!me->path)
goto err;
return monitor_modify_epoll(mn, me, TRUE);
err:
rc = -errno;
free_monitor_entry(me);
DBG(MONITOR, ul_debugobj(mn, "failed to allocate kernel monitor [rc=%d]", rc));
return rc;
}
/*
* Add/Remove monitor entry to/from monitor epoll.
*/
static int monitor_modify_epoll(struct libmnt_monitor *mn,
struct monitor_entry *me, int enable)
{
assert(mn);
assert(me);
me->enable = enable ? 1 : 0;
me->changed = 0;
if (mn->fd < 0)
return 0; /* no epoll, ignore request */
if (enable) {
struct epoll_event ev = { .events = me->events };
int fd = me->opers->op_get_fd(mn, me);
if (fd < 0)
goto err;
DBG(MONITOR, ul_debugobj(mn, " add fd=%d (for %s)", fd, me->path));
ev.data.ptr = (void *) me;
if (epoll_ctl(mn->fd, EPOLL_CTL_ADD, fd, &ev) < 0) {
if (errno != EEXIST)
goto err;
}
if (me->events & (EPOLLIN | EPOLLET)) {
/* Drain initial events generated for /proc/self/mountinfo */
struct epoll_event events[1];
while (epoll_wait(mn->fd, events, 1, 0) > 0);
}
} else if (me->fd) {
DBG(MONITOR, ul_debugobj(mn, " remove fd=%d (for %s)", me->fd, me->path));
if (epoll_ctl(mn->fd, EPOLL_CTL_DEL, me->fd, NULL) < 0) {
if (errno != ENOENT)
goto err;
}
}
return 0;
err:
return -errno;
}
/**
* mnt_monitor_close_fd:
* @mn: monitor
*
* Close monitor file descriptor. This is usually unnecessary, because
* mnt_unref_monitor() cleanups all.
*
* The function is necessary only if you want to reset monitor setting. The
* next mnt_monitor_get_fd() or mnt_monitor_wait() will use newly initialized
* monitor. This restart is unnecessary for mnt_monitor_enable_*() functions.
*
* Returns: 0 on success, <0 on error.
*/
int mnt_monitor_close_fd(struct libmnt_monitor *mn)
{
struct libmnt_iter itr;
struct monitor_entry *me;
if (!mn)
return -EINVAL;
mnt_reset_iter(&itr, MNT_ITER_FORWARD);
/* disable all monitor entries */
while (monitor_next_entry(mn, &itr, &me) == 0) {
/* remove entry from epoll */
if (mn->fd >= 0)
monitor_modify_epoll(mn, me, FALSE);
/* close entry FD */
me->opers->op_close_fd(mn, me);
}
if (mn->fd >= 0) {
DBG(MONITOR, ul_debugobj(mn, "closing top-level monitor fd"));
close(mn->fd);
}
mn->fd = -1;
return 0;
}
/**
* mnt_monitor_get_fd:
* @mn: monitor
*
* The file descriptor is associated with all monitored files and it's usable
* for example for epoll. You have to call mnt_monitor_event_cleanup() or
* mnt_monitor_next_change() after each event.
*
* Returns: >=0 (fd) on success, <0 on error
*/
int mnt_monitor_get_fd(struct libmnt_monitor *mn)
{
struct libmnt_iter itr;
struct monitor_entry *me;
int rc = 0;
if (!mn)
return -EINVAL;
if (mn->fd >= 0)
return mn->fd;
DBG(MONITOR, ul_debugobj(mn, "create top-level monitor fd"));
mn->fd = epoll_create1(EPOLL_CLOEXEC);
if (mn->fd < 0)
return -errno;
mnt_reset_iter(&itr, MNT_ITER_FORWARD);
DBG(MONITOR, ul_debugobj(mn, "adding monitor entries to epoll (fd=%d)", mn->fd));
while (monitor_next_entry(mn, &itr, &me) == 0) {
if (!me->enable)
continue;
rc = monitor_modify_epoll(mn, me, TRUE);
if (rc)
goto err;
}
DBG(MONITOR, ul_debugobj(mn, "successfully created monitor"));
return mn->fd;
err:
rc = errno ? -errno : -EINVAL;
close(mn->fd);
mn->fd = -1;
DBG(MONITOR, ul_debugobj(mn, "failed to create monitor [rc=%d]", rc));
return rc;
}
/**
* mnt_monitor_wait:
* @mn: monitor
* @timeout: number of milliseconds, -1 block indefinitely, 0 return immediately
*
* Waits for the next change, after the event it's recommended to use
* mnt_monitor_next_change() to get more details about the change and to
* avoid false positive events.
*
* Returns: 1 success (something changed), 0 timeout, <0 error.
*/
int mnt_monitor_wait(struct libmnt_monitor *mn, int timeout)
{
int rc;
struct monitor_entry *me;
struct epoll_event events[1];
if (!mn)
return -EINVAL;
if (mn->fd < 0) {
rc = mnt_monitor_get_fd(mn);
if (rc < 0)
return rc;
}
do {
DBG(MONITOR, ul_debugobj(mn, "calling epoll_wait(), timeout=%d", timeout));
rc = epoll_wait(mn->fd, events, 1, timeout);
if (rc < 0)
return -errno; /* error */
if (rc == 0)
return 0; /* timeout */
me = (struct monitor_entry *) events[0].data.ptr;
if (!me)
return -EINVAL;
if (me->opers->op_event_verify == NULL ||
me->opers->op_event_verify(mn, me) == 1) {
me->changed = 1;
break;
}
} while (1);
return 1; /* success */
}
static struct monitor_entry *get_changed(struct libmnt_monitor *mn)
{
struct libmnt_iter itr;
struct monitor_entry *me;
mnt_reset_iter(&itr, MNT_ITER_FORWARD);
while (monitor_next_entry(mn, &itr, &me) == 0) {
if (me->changed)
return me;
}
return NULL;
}
/**
* mnt_monitor_next_change:
* @mn: monitor
* @filename: returns changed file (optional argument)
* @type: returns MNT_MONITOR_TYPE_* (optional argument)
*
* The function does not wait and it's designed to provide details about changes.
* It's always recommended to use this function to avoid false positives.
*
* Returns: 0 on success, 1 no change, <0 on error
*/
int mnt_monitor_next_change(struct libmnt_monitor *mn,
const char **filename,
int *type)
{
int rc;
struct monitor_entry *me;
if (!mn || mn->fd < 0)
return -EINVAL;
/*
* if we previously called epoll_wait() (e.g. mnt_monitor_wait()) then
* info about unread change is already stored in monitor_entry.
*
* If we get nothing, then ask kernel.
*/
me = get_changed(mn);
while (!me) {
struct epoll_event events[1];
DBG(MONITOR, ul_debugobj(mn, "asking for next changed"));
rc = epoll_wait(mn->fd, events, 1, 0); /* no timeout! */
if (rc < 0) {
DBG(MONITOR, ul_debugobj(mn, " *** error"));
return -errno;
}
if (rc == 0) {
DBG(MONITOR, ul_debugobj(mn, " *** nothing"));
return 1;
}
me = (struct monitor_entry *) events[0].data.ptr;
if (!me)
return -EINVAL;
if (me->opers->op_event_verify != NULL &&
me->opers->op_event_verify(mn, me) != 1)
me = NULL;
}
me->changed = 0;
if (filename)
*filename = me->path;
if (type)
*type = me->type;
DBG(MONITOR, ul_debugobj(mn, " *** success [changed: %s]", me->path));
return 0; /* success */
}
/**
* mnt_monitor_event_cleanup:
* @mn: monitor
*
* This function cleanups (drain) internal buffers. It's necessary to call
* this function after event if you do not call mnt_monitor_next_change().
*
* Returns: 0 on success, <0 on error
*/
int mnt_monitor_event_cleanup(struct libmnt_monitor *mn)
{
int rc;
if (!mn || mn->fd < 0)
return -EINVAL;
while ((rc = mnt_monitor_next_change(mn, NULL, NULL)) == 0);
return rc < 0 ? rc : 0;
}
#ifdef TEST_PROGRAM
static struct libmnt_monitor *create_test_monitor(int argc, char *argv[])
{
struct libmnt_monitor *mn;
int i;
mn = mnt_new_monitor();
if (!mn) {
warn("failed to allocate monitor");
goto err;
}
for (i = 1; i < argc; i++) {
if (strcmp(argv[i], "userspace") == 0) {
if (mnt_monitor_enable_userspace(mn, TRUE, NULL)) {
warn("failed to initialize userspace monitor");
goto err;
}
} else if (strcmp(argv[i], "kernel") == 0) {
if (mnt_monitor_enable_kernel(mn, TRUE)) {
warn("failed to initialize kernel monitor");
goto err;
}
}
}
if (i == 1) {
warnx("No monitor type specified");
goto err;
}
return mn;
err:
mnt_unref_monitor(mn);
return NULL;
}
/*
* create a monitor and add the monitor fd to epoll
*/
static int __test_epoll(struct libmnt_test *ts, int argc, char *argv[], int cleanup)
{
int fd, efd = -1, rc = -1;
struct epoll_event ev;
struct libmnt_monitor *mn = create_test_monitor(argc, argv);
if (!mn)
return -1;
fd = mnt_monitor_get_fd(mn);
if (fd < 0) {
warn("failed to initialize monitor fd");
goto done;
}
efd = epoll_create1(EPOLL_CLOEXEC);
if (efd < 0) {
warn("failed to create epoll");
goto done;
}
ev.events = EPOLLIN;
ev.data.fd = fd;
rc = epoll_ctl(efd, EPOLL_CTL_ADD, fd, &ev);
if (rc < 0) {
warn("failed to add fd to epoll");
goto done;
}
printf("waiting for changes...\n");
do {
const char *filename = NULL;
struct epoll_event events[1];
int n = epoll_wait(efd, events, 1, -1);
if (n < 0) {
rc = -errno;
warn("polling error");
goto done;
}
if (n == 0 || events[0].data.fd != fd)
continue;
printf(" top-level FD active\n");
if (cleanup)
mnt_monitor_event_cleanup(mn);
else {
while (mnt_monitor_next_change(mn, &filename, NULL) == 0)
printf(" %s: change detected\n", filename);
}
} while (1);
rc = 0;
done:
if (efd >= 0)
close(efd);
mnt_unref_monitor(mn);
return rc;
}
/*
* create a monitor and add the monitor fd to epoll
*/
static int test_epoll(struct libmnt_test *ts, int argc, char *argv[])
{
return __test_epoll(ts, argc, argv, 0);
}
static int test_epoll_cleanup(struct libmnt_test *ts, int argc, char *argv[])
{
return __test_epoll(ts, argc, argv, 1);
}
/*
* create a monitor and wait for a change
*/
static int test_wait(struct libmnt_test *ts, int argc, char *argv[])
{
const char *filename;
struct libmnt_monitor *mn = create_test_monitor(argc, argv);
if (!mn)
return -1;
printf("waiting for changes...\n");
while (mnt_monitor_wait(mn, -1) > 0) {
printf("notification detected\n");
while (mnt_monitor_next_change(mn, &filename, NULL) == 0)
printf(" %s: change detected\n", filename);
}
mnt_unref_monitor(mn);
return 0;
}
int main(int argc, char *argv[])
{
struct libmnt_test tss[] = {
{ "--epoll", test_epoll, "<userspace kernel ...> monitor in epoll" },
{ "--epoll-clean", test_epoll_cleanup, "<userspace kernel ...> monitor in epoll and clean events" },
{ "--wait", test_wait, "<userspace kernel ...> monitor wait function" },
{ NULL }
};
return mnt_run_test(tss, argc, argv);
}
#endif /* TEST_PROGRAM */