1 /*
2 * Copyright © 2025 Igalia S.L.
3 * SPDX-License-Identifier: MIT
4 */
5
6 #include "os_file_notify.h"
7 #include "detect_os.h"
8 #include "log.h"
9 #include "u_thread.h"
10
11 #if DETECT_OS_LINUX
12 #include <sys/eventfd.h>
13 #include <sys/inotify.h>
14 #include <sys/poll.h>
15
16 #include <errno.h>
17 #include <stdatomic.h>
18 #include <stdlib.h>
19
20 struct os_file_notifier {
21 int ifd;
22 int file_wd, dir_wd;
23 int efd;
24 os_file_notify_cb cb;
25 void *data;
26 thrd_t thread;
27 atomic_bool quit;
28 const char *filename;
29 char file_path[PATH_MAX], dir_path[PATH_MAX];
30 };
31
32 #define INOTIFY_BUF_LEN ((10 * (sizeof(struct inotify_event) + NAME_MAX + 1)))
33
34 static int
os_file_notifier_thread(void * data)35 os_file_notifier_thread(void *data)
36 {
37 os_file_notifier_t notifier = data;
38 char buf[INOTIFY_BUF_LEN]
39 __attribute__((aligned(__alignof__(struct inotify_event))));
40 ssize_t len;
41
42 u_thread_setname("File Notifier");
43
44 /* To ensure the callback sets the file up to the initial state. */
45 if (access(notifier->file_path, F_OK) != -1) {
46 notifier->cb(notifier->data, notifier->file_path, false, false, false);
47 } else {
48 /* The file doesn't exist, we cannot watch it. */
49 notifier->cb(notifier->data, notifier->file_path, false, true, false);
50 }
51
52 while (!notifier->quit) {
53 /* Poll on the inotify file descriptor and the eventfd file descriptor. */
54 struct pollfd fds[] = {
55 {notifier->ifd, POLLIN, 0},
56 {notifier->efd, POLLIN, 0},
57 };
58 if (poll(fds, ARRAY_SIZE(fds), -1) == -1) {
59 if (errno == EINTR || errno == EAGAIN)
60 continue;
61
62 mesa_logw("Failed to poll on file notifier FDs: %s",
63 strerror(errno));
64 return -1;
65 }
66
67 if (fds[1].revents & POLLIN) {
68 /* The eventfd is used to wake up the thread when the notifier is
69 * destroyed. */
70 eventfd_t val;
71 eventfd_read(notifier->efd, &val);
72 if (val == 1)
73 return 0; /* Quit the thread. */
74 }
75
76 len = read(notifier->ifd, buf, sizeof(buf));
77 if (len == -1) {
78 if (errno == EINTR || errno == EAGAIN)
79 continue;
80
81 mesa_logw("Failed to read inotify events: %s", strerror(errno));
82 return -1;
83 } else {
84 for (char *ptr = buf; ptr < buf + len;
85 ptr += ((struct inotify_event *)ptr)->len +
86 sizeof(struct inotify_event)) {
87 struct inotify_event *event = (struct inotify_event *)ptr;
88
89 bool created = false, deleted = false, dir_deleted = false;
90 if (event->wd == notifier->dir_wd) {
91 /* Check if the event is about the directory itself or the
92 * file. */
93 if (event->mask & (IN_DELETE_SELF | IN_MOVE_SELF)) {
94 /* The directory was deleted/moved, we cannot watch it or
95 * the file anymore. */
96 dir_deleted = true;
97 } else if (strcmp(event->name, notifier->filename) == 0) {
98 /* If it's a file event, ensure that the event is about the
99 * file we are watching. */
100 if (event->mask & IN_CREATE) {
101 created = true;
102
103 /* The file was just created, add a watch to it. */
104 notifier->file_wd = inotify_add_watch(
105 notifier->ifd, notifier->file_path, IN_CLOSE_WRITE);
106 if (notifier->file_wd == -1) {
107 mesa_logw("Failed to add inotify watch for file");
108 return -1;
109 }
110 } else if (event->mask & IN_DELETE) {
111 deleted = true;
112
113 /* The file was deleted, we cannot watch it anymore. */
114 inotify_rm_watch(notifier->ifd, notifier->file_wd);
115 notifier->file_wd = -1;
116 }
117 }
118 }
119
120 notifier->cb(notifier->data, notifier->file_path, created,
121 deleted, dir_deleted);
122 if (dir_deleted)
123 return 0;
124 }
125 }
126 }
127
128 return 0;
129 }
130
131 os_file_notifier_t
os_file_notifier_create(const char * file_path,os_file_notify_cb cb,void * data,const char ** error_str)132 os_file_notifier_create(const char *file_path, os_file_notify_cb cb,
133 void *data, const char **error_str)
134 {
135 #define RET_ERR(s) \
136 do { \
137 if (error_str) { \
138 *error_str = s; \
139 } \
140 goto cleanup; \
141 } while (0)
142
143 os_file_notifier_t notifier = calloc(1, sizeof(struct os_file_notifier));
144 if (!notifier)
145 RET_ERR("Failed to allocate memory for file notifier");
146 notifier->ifd = -1;
147 notifier->efd = -1;
148
149 size_t path_len = strlen(file_path);
150 if (path_len == 0)
151 RET_ERR("File path is empty");
152 else if (path_len >= PATH_MAX)
153 RET_ERR("File path is longer than PATH_MAX");
154
155 memcpy(notifier->file_path, file_path, path_len + 1);
156
157 notifier->ifd = inotify_init1(IN_NONBLOCK);
158 if (notifier->ifd == -1)
159 RET_ERR("Failed to initialize inotify");
160
161 notifier->file_wd =
162 inotify_add_watch(notifier->ifd, notifier->file_path, IN_CLOSE_WRITE);
163 if (notifier->file_wd == -1 && errno != ENOENT)
164 RET_ERR("Failed to add inotify watch for file");
165
166 /* Determine the parent directory path of the file. */
167 char *last_slash = strrchr(notifier->file_path, '/');
168 if (last_slash) {
169 size_t len = last_slash - notifier->file_path;
170 memcpy(notifier->dir_path, notifier->file_path, len);
171 notifier->dir_path[len] = 0;
172 notifier->filename = last_slash + 1;
173 } else {
174 notifier->dir_path[0] = '.';
175 notifier->dir_path[1] = 0;
176 notifier->filename = notifier->file_path;
177 }
178
179 notifier->dir_wd =
180 inotify_add_watch(notifier->ifd, notifier->dir_path,
181 IN_CREATE | IN_MOVE | IN_DELETE | IN_DELETE_SELF |
182 IN_MOVE_SELF | IN_ONLYDIR);
183 if (notifier->dir_wd == -1) {
184 if (errno == ENOENT)
185 RET_ERR("The folder containing the watched file doesn't exist");
186 RET_ERR("Failed to add inotify watch for directory");
187 }
188
189 notifier->efd = eventfd(0, EFD_NONBLOCK);
190 if (notifier->efd == -1)
191 RET_ERR("Failed to create eventfd");
192
193 notifier->cb = cb;
194 notifier->data = data;
195
196 if (u_thread_create(¬ifier->thread, os_file_notifier_thread,
197 notifier) != 0)
198 RET_ERR("Failed to create file notifier thread");
199
200 return notifier;
201
202 cleanup:
203 if (notifier) {
204 if (notifier->ifd != -1)
205 close(notifier->ifd);
206 if (notifier->efd != -1)
207 close(notifier->efd);
208 free(notifier);
209 }
210 return NULL;
211
212 #undef RET_ERR
213 }
214
215 void
os_file_notifier_destroy(os_file_notifier_t notifier)216 os_file_notifier_destroy(os_file_notifier_t notifier)
217 {
218 if (!notifier)
219 return;
220
221 notifier->quit = true;
222 eventfd_write(notifier->efd, 1);
223 thrd_join(notifier->thread, NULL);
224
225 close(notifier->ifd);
226 close(notifier->efd);
227 free(notifier);
228 }
229
230 #else /* !DETECT_OS_LINUX */
231
232 os_file_notifier_t
os_file_notifier_create(const char * file_path,os_file_notify_cb cb,void * data,const char ** error_str)233 os_file_notifier_create(const char *file_path, os_file_notify_cb cb,
234 void *data, const char **error_str)
235 {
236 (void)file_path;
237 (void)cb;
238 (void)data;
239
240 return NULL;
241 }
242
243 void
os_file_notifier_destroy(os_file_notifier_t notifier)244 os_file_notifier_destroy(os_file_notifier_t notifier)
245 {
246 (void)notifier;
247 }
248
249 #endif