util: Add file modification notifier utility
A helper to watch files for changes or deletions using inotify, with a callback mechanism to notify the user of changes. Signed-off-by: Mark Collins <mark@igalia.com> Part-of: <https://gitlab.freedesktop.org/mesa/mesa/-/merge_requests/32906>
This commit is contained in:
@@ -74,6 +74,7 @@ files_mesa_util = files(
|
||||
'os_time.c',
|
||||
'os_time.h',
|
||||
'os_file.c',
|
||||
'os_file_notify.c',
|
||||
'os_memory_fd.c',
|
||||
'os_misc.c',
|
||||
'os_misc.h',
|
||||
|
||||
249
src/util/os_file_notify.c
Normal file
249
src/util/os_file_notify.c
Normal file
@@ -0,0 +1,249 @@
|
||||
/*
|
||||
* Copyright © 2025 Igalia S.L.
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
#include "os_file_notify.h"
|
||||
#include "detect_os.h"
|
||||
#include "log.h"
|
||||
#include "u_thread.h"
|
||||
|
||||
#if DETECT_OS_LINUX
|
||||
#include <sys/eventfd.h>
|
||||
#include <sys/inotify.h>
|
||||
#include <sys/poll.h>
|
||||
|
||||
#include <errno.h>
|
||||
#include <stdatomic.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
struct os_file_notifier {
|
||||
int ifd;
|
||||
int file_wd, dir_wd;
|
||||
int efd;
|
||||
os_file_notify_cb cb;
|
||||
void *data;
|
||||
thrd_t thread;
|
||||
atomic_bool quit;
|
||||
const char *filename;
|
||||
char file_path[PATH_MAX], dir_path[PATH_MAX];
|
||||
};
|
||||
|
||||
#define INOTIFY_BUF_LEN ((10 * (sizeof(struct inotify_event) + NAME_MAX + 1)))
|
||||
|
||||
static int
|
||||
os_file_notifier_thread(void *data)
|
||||
{
|
||||
os_file_notifier_t notifier = data;
|
||||
char buf[INOTIFY_BUF_LEN]
|
||||
__attribute__((aligned(__alignof__(struct inotify_event))));
|
||||
ssize_t len;
|
||||
|
||||
u_thread_setname("File Notifier");
|
||||
|
||||
/* To ensure the callback sets the file up to the initial state. */
|
||||
if (access(notifier->file_path, F_OK) != -1) {
|
||||
notifier->cb(notifier->data, notifier->file_path, false, false, false);
|
||||
} else {
|
||||
/* The file doesn't exist, we cannot watch it. */
|
||||
notifier->cb(notifier->data, notifier->file_path, false, true, false);
|
||||
}
|
||||
|
||||
while (!notifier->quit) {
|
||||
/* Poll on the inotify file descriptor and the eventfd file descriptor. */
|
||||
struct pollfd fds[] = {
|
||||
{notifier->ifd, POLLIN, 0},
|
||||
{notifier->efd, POLLIN, 0},
|
||||
};
|
||||
if (poll(fds, ARRAY_SIZE(fds), -1) == -1) {
|
||||
if (errno == EINTR || errno == EAGAIN)
|
||||
continue;
|
||||
|
||||
mesa_logw("Failed to poll on file notifier FDs: %s",
|
||||
strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (fds[1].revents & POLLIN) {
|
||||
/* The eventfd is used to wake up the thread when the notifier is
|
||||
* destroyed. */
|
||||
eventfd_t val;
|
||||
eventfd_read(notifier->efd, &val);
|
||||
if (val == 1)
|
||||
return 0; /* Quit the thread. */
|
||||
}
|
||||
|
||||
len = read(notifier->ifd, buf, sizeof(buf));
|
||||
if (len == -1) {
|
||||
if (errno == EINTR || errno == EAGAIN)
|
||||
continue;
|
||||
|
||||
mesa_logw("Failed to read inotify events: %s", strerror(errno));
|
||||
return -1;
|
||||
} else {
|
||||
for (char *ptr = buf; ptr < buf + len;
|
||||
ptr += ((struct inotify_event *)ptr)->len +
|
||||
sizeof(struct inotify_event)) {
|
||||
struct inotify_event *event = (struct inotify_event *)ptr;
|
||||
|
||||
bool created = false, deleted = false, dir_deleted = false;
|
||||
if (event->wd == notifier->dir_wd) {
|
||||
/* Check if the event is about the directory itself or the
|
||||
* file. */
|
||||
if (event->mask & (IN_DELETE_SELF | IN_MOVE_SELF)) {
|
||||
/* The directory was deleted/moved, we cannot watch it or
|
||||
* the file anymore. */
|
||||
dir_deleted = true;
|
||||
} else if (strcmp(event->name, notifier->filename) == 0) {
|
||||
/* If it's a file event, ensure that the event is about the
|
||||
* file we are watching. */
|
||||
if (event->mask & IN_CREATE) {
|
||||
created = true;
|
||||
|
||||
/* The file was just created, add a watch to it. */
|
||||
notifier->file_wd = inotify_add_watch(
|
||||
notifier->ifd, notifier->file_path, IN_CLOSE_WRITE);
|
||||
if (notifier->file_wd == -1) {
|
||||
mesa_logw("Failed to add inotify watch for file");
|
||||
return -1;
|
||||
}
|
||||
} else if (event->mask & IN_DELETE) {
|
||||
deleted = true;
|
||||
|
||||
/* The file was deleted, we cannot watch it anymore. */
|
||||
inotify_rm_watch(notifier->ifd, notifier->file_wd);
|
||||
notifier->file_wd = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
notifier->cb(notifier->data, notifier->file_path, created,
|
||||
deleted, dir_deleted);
|
||||
if (dir_deleted)
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
os_file_notifier_t
|
||||
os_file_notifier_create(const char *file_path, os_file_notify_cb cb,
|
||||
void *data, const char **error_str)
|
||||
{
|
||||
#define RET_ERR(s) \
|
||||
do { \
|
||||
if (error_str) { \
|
||||
*error_str = s; \
|
||||
} \
|
||||
goto cleanup; \
|
||||
} while (0)
|
||||
|
||||
os_file_notifier_t notifier = calloc(1, sizeof(struct os_file_notifier));
|
||||
if (!notifier)
|
||||
RET_ERR("Failed to allocate memory for file notifier");
|
||||
notifier->ifd = -1;
|
||||
notifier->efd = -1;
|
||||
|
||||
size_t path_len = strlen(file_path);
|
||||
if (path_len == 0)
|
||||
RET_ERR("File path is empty");
|
||||
else if (path_len >= PATH_MAX)
|
||||
RET_ERR("File path is longer than PATH_MAX");
|
||||
|
||||
memcpy(notifier->file_path, file_path, path_len + 1);
|
||||
|
||||
notifier->ifd = inotify_init1(IN_NONBLOCK);
|
||||
if (notifier->ifd == -1)
|
||||
RET_ERR("Failed to initialize inotify");
|
||||
|
||||
notifier->file_wd =
|
||||
inotify_add_watch(notifier->ifd, notifier->file_path, IN_CLOSE_WRITE);
|
||||
if (notifier->file_wd == -1 && errno != ENOENT)
|
||||
RET_ERR("Failed to add inotify watch for file");
|
||||
|
||||
/* Determine the parent directory path of the file. */
|
||||
char *last_slash = strrchr(notifier->file_path, '/');
|
||||
if (last_slash) {
|
||||
size_t len = last_slash - notifier->file_path;
|
||||
memcpy(notifier->dir_path, notifier->file_path, len);
|
||||
notifier->dir_path[len] = 0;
|
||||
notifier->filename = last_slash + 1;
|
||||
} else {
|
||||
notifier->dir_path[0] = '.';
|
||||
notifier->dir_path[1] = 0;
|
||||
notifier->filename = notifier->file_path;
|
||||
}
|
||||
|
||||
notifier->dir_wd =
|
||||
inotify_add_watch(notifier->ifd, notifier->dir_path,
|
||||
IN_CREATE | IN_MOVE | IN_DELETE | IN_DELETE_SELF |
|
||||
IN_MOVE_SELF | IN_ONLYDIR);
|
||||
if (notifier->dir_wd == -1) {
|
||||
if (errno == ENOENT)
|
||||
RET_ERR("The folder containing the watched file doesn't exist");
|
||||
RET_ERR("Failed to add inotify watch for directory");
|
||||
}
|
||||
|
||||
notifier->efd = eventfd(0, EFD_NONBLOCK);
|
||||
if (notifier->efd == -1)
|
||||
RET_ERR("Failed to create eventfd");
|
||||
|
||||
notifier->cb = cb;
|
||||
notifier->data = data;
|
||||
|
||||
if (u_thread_create(¬ifier->thread, os_file_notifier_thread,
|
||||
notifier) != 0)
|
||||
RET_ERR("Failed to create file notifier thread");
|
||||
|
||||
return notifier;
|
||||
|
||||
cleanup:
|
||||
if (notifier) {
|
||||
if (notifier->ifd != -1)
|
||||
close(notifier->ifd);
|
||||
if (notifier->efd != -1)
|
||||
close(notifier->efd);
|
||||
free(notifier);
|
||||
}
|
||||
return NULL;
|
||||
|
||||
#undef RET_ERR
|
||||
}
|
||||
|
||||
void
|
||||
os_file_notifier_destroy(os_file_notifier_t notifier)
|
||||
{
|
||||
if (!notifier)
|
||||
return;
|
||||
|
||||
notifier->quit = true;
|
||||
eventfd_write(notifier->efd, 1);
|
||||
thrd_join(notifier->thread, NULL);
|
||||
|
||||
close(notifier->ifd);
|
||||
close(notifier->efd);
|
||||
free(notifier);
|
||||
}
|
||||
|
||||
#else /* !DETECT_OS_LINUX */
|
||||
|
||||
os_file_notifier_t
|
||||
os_file_notifier_create(const char *file_path, os_file_notify_cb cb,
|
||||
void *data, const char **error_str)
|
||||
{
|
||||
(void)file_path;
|
||||
(void)cb;
|
||||
(void)data;
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void
|
||||
os_file_notifier_destroy(os_file_notifier_t notifier)
|
||||
{
|
||||
(void)notifier;
|
||||
}
|
||||
|
||||
#endif
|
||||
52
src/util/os_file_notify.h
Normal file
52
src/util/os_file_notify.h
Normal file
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright © 2025 Igalia S.L.
|
||||
* SPDX-License-Identifier: MIT
|
||||
*
|
||||
* File modification and deletion notification mechanism.
|
||||
*/
|
||||
|
||||
#ifndef _OS_FILE_NOTIFY_H_
|
||||
#define _OS_FILE_NOTIFY_H_
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
struct os_file_notifier;
|
||||
typedef struct os_file_notifier *os_file_notifier_t;
|
||||
|
||||
/*
|
||||
* Callback function for file notification.
|
||||
* The `data` parameter is the same as the one passed to os_file_notifier_create().
|
||||
* The `path` parameter is the path of the file that was modified.
|
||||
* The `created` parameter is true if the file was created.
|
||||
* The `deleted` parameter is true if the file was deleted.
|
||||
* The `dir_deleted` parameter is true if the file's parent directory was deleted. No further events will be delivered.
|
||||
*/
|
||||
typedef void (*os_file_notify_cb)(void *data, const char *path, bool created, bool deleted, bool dir_deleted);
|
||||
|
||||
/*
|
||||
* Create a new file notifier which watches the file at the specified path.
|
||||
* If a file notifier cannot be created, NULL is returned with `error_str` (if non-NULL) set to an error message.
|
||||
* Note: The folder must already exist, if the folder containing the file doesn't exist this will fail.
|
||||
* If the folder is deleted after the file notifier is created, the file notifier will no longer deliver events.
|
||||
* If the file is deleted and recreated, the file notifier will deliver a deletion event followed by a creation event.
|
||||
* The file notifier always delivers an event at startup. If the file doesn't exist, the `deleted` parameter will be true.
|
||||
*/
|
||||
os_file_notifier_t
|
||||
os_file_notifier_create(const char *path, os_file_notify_cb cb, void *data, const char **error_str);
|
||||
|
||||
/*
|
||||
* Destroy a file notifier.
|
||||
*/
|
||||
void
|
||||
os_file_notifier_destroy(os_file_notifier_t notifier);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* _OS_FILE_NOTIFY_H_ */
|
||||
Reference in New Issue
Block a user