fsmonitor: implement filesystem change listener for Linux

Implement the built-in fsmonitor daemon for Linux using the inotify API, bringing it to feature parity with the existing Windows and macOS implementations. The implementation uses inotify rather than fanotify because fanotify requires either CAP_SYS_ADMIN or CAP_PERFMON capabilities, making it unsuitable for an unprivileged user-space daemon. While inotify has the limitation of requiring a separate watch on every directory (unlike macOS's FSEvents, which can monitor an entire directory tree with a single watch), it operates without elevated privileges and provides the per-file event granularity needed for fsmonitor. The listener uses inotify_init1(O_NONBLOCK) with a poll loop that checks for events with a 50-millisecond timeout, keeping the inotify queue well-drained to minimize the risk of overflows. Bidirectional hashmaps map between watch descriptors and directory paths for efficient event resolution. Directory renames are tracked using inotify's cookie mechanism to correlate IN_MOVED_FROM and IN_MOVED_TO event pairs; a periodic check detects stale renames where the matching IN_MOVED_TO never arrived, forcing a resync. New directory creation triggers recursive watch registration to ensure all subdirectories are monitored. The IN_MASK_CREATE flag is used where available to prevent modifying existing watches, with a fallback for older kernels. When IN_MASK_CREATE is available and inotify_add_watch returns EEXIST, it means another thread or recursive scan has already registered the watch, so it is safe to ignore. Remote filesystem detection uses statfs() to identify network-mounted filesystems (NFS, CIFS, SMB, FUSE, etc.) via their magic numbers. Mount point information is read from /proc/mounts and matched against the statfs f_fsid to get accurate, human-readable filesystem type names for logging. When the .git directory is on a remote filesystem, the IPC socket falls back to $HOME or a user-configured directory via the fsmonitor.socketDir setting. Based-on-patch-by: Eric DeCosta <edecosta@mathworks.com> Based-on-patch-by: Marziyeh Esipreh <marziyeh.esipreh@gmail.com> Signed-off-by: Paul Tarjan <github@paulisageek.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Paul Tarjan committed Apr 15, 2026 at 13:27 UTC ce48de8b2c85a4e5cbeb5dd1f2cfe042dd5392e4
8 files changed +1042 -8
Documentation/config/fsmonitor--daemon.adoc
+2 -2
@@ -4,8 +4,8 @@ fsmonitor.allowRemote::
4 behavior. Only respected when `core.fsmonitor` is set to `true`.
5
6 fsmonitor.socketDir::
7 - This Mac OS-specific option, if set, specifies the directory in
7 + This Mac OS and Linux-specific option, if set, specifies the directory in
8 which to create the Unix domain socket used for communication
9 between the fsmonitor daemon and various Git commands. The directory must
10 - reside on a native Mac OS filesystem. Only respected when `core.fsmonitor`
10 + reside on a native filesystem. Only respected when `core.fsmonitor`
11 is set to `true`.
Documentation/git-fsmonitor--daemon.adoc
+24 -4
@@ -76,9 +76,9 @@ repositories; this may be overridden by setting `fsmonitor.allowRemote` to
76 correctly with all network-mounted repositories, so such use is considered
77 experimental.
78
79 -On Mac OS, the inter-process communication (IPC) between various Git
79 +On Mac OS and Linux, the inter-process communication (IPC) between various Git
80 commands and the fsmonitor daemon is done via a Unix domain socket (UDS) -- a
81 -special type of file -- which is supported by native Mac OS filesystems,
81 +special type of file -- which is supported by native Mac OS and Linux filesystems,
82 but not on network-mounted filesystems, NTFS, or FAT32. Other filesystems
83 may or may not have the needed support; the fsmonitor daemon is not guaranteed
84 to work with these filesystems and such use is considered experimental.
@@ -87,13 +87,33 @@ By default, the socket is created in the `.git` directory. However, if the
87 `.git` directory is on a network-mounted filesystem, it will instead be
88 created at `$HOME/.git-fsmonitor-*` unless `$HOME` itself is on a
89 network-mounted filesystem, in which case you must set the configuration
90 -variable `fsmonitor.socketDir` to the path of a directory on a Mac OS native
90 +variable `fsmonitor.socketDir` to the path of a directory on a native
91 filesystem in which to create the socket file.
92
93 If none of the above directories (`.git`, `$HOME`, or `fsmonitor.socketDir`)
94 -is on a native Mac OS file filesystem the fsmonitor daemon will report an
94 +is on a native filesystem the fsmonitor daemon will report an
95 error that will cause the daemon and the currently running command to exit.
96
97 +LINUX CAVEATS
98 +~~~~~~~~~~~~~
99 +
100 +On Linux, the fsmonitor daemon uses inotify to monitor filesystem events.
101 +The inotify system has per-user limits on the number of watches that can
102 +be created. The default limit is typically 8192 watches per user.
103 +
104 +For large repositories with many directories, you may need to increase
105 +this limit. Check the current limit with:
106 +
107 + cat /proc/sys/fs/inotify/max_user_watches
108 +
109 +To temporarily increase the limit:
110 +
111 + sudo sysctl fs.inotify.max_user_watches=65536
112 +
113 +To make the change permanent, add to `/etc/sysctl.conf`:
114 +
115 + fs.inotify.max_user_watches=65536
116 +
117 CONFIGURATION
118 -------------
119
compat/fsmonitor/fsm-health-linux.c new
+33
@@ -0,0 +1,33 @@
1 +#include "git-compat-util.h"
2 +#include "config.h"
3 +#include "fsmonitor-ll.h"
4 +#include "fsm-health.h"
5 +#include "fsmonitor--daemon.h"
6 +
7 +/*
8 + * The Linux fsmonitor implementation uses inotify which has its own
9 + * mechanisms for detecting filesystem unmount and other events that
10 + * would require the daemon to shutdown. Therefore, we don't need
11 + * a separate health thread like Windows does.
12 + *
13 + * These stub functions satisfy the interface requirements.
14 + */
15 +
16 +int fsm_health__ctor(struct fsmonitor_daemon_state *state UNUSED)
17 +{
18 + return 0;
19 +}
20 +
21 +void fsm_health__dtor(struct fsmonitor_daemon_state *state UNUSED)
22 +{
23 + return;
24 +}
25 +
26 +void fsm_health__loop(struct fsmonitor_daemon_state *state UNUSED)
27 +{
28 + return;
29 +}
30 +
31 +void fsm_health__stop_async(struct fsmonitor_daemon_state *state UNUSED)
32 +{
33 +}
compat/fsmonitor/fsm-listen-linux.c new
+746
@@ -0,0 +1,746 @@
1 +#include "git-compat-util.h"
2 +#include "dir.h"
3 +#include "fsmonitor-ll.h"
4 +#include "fsm-listen.h"
5 +#include "fsmonitor--daemon.h"
6 +#include "fsmonitor-path-utils.h"
7 +#include "gettext.h"
8 +#include "simple-ipc.h"
9 +#include "string-list.h"
10 +#include "trace.h"
11 +
12 +#include <sys/inotify.h>
13 +
14 +/*
15 + * Safe value to bitwise OR with rest of mask for
16 + * kernels that do not support IN_MASK_CREATE
17 + */
18 +#ifndef IN_MASK_CREATE
19 +#define IN_MASK_CREATE 0x00000000
20 +#endif
21 +
22 +enum shutdown_reason {
23 + SHUTDOWN_CONTINUE = 0,
24 + SHUTDOWN_STOP,
25 + SHUTDOWN_ERROR,
26 + SHUTDOWN_FORCE
27 +};
28 +
29 +struct watch_entry {
30 + struct hashmap_entry ent;
31 + int wd;
32 + uint32_t cookie;
33 + const char *dir;
34 +};
35 +
36 +struct rename_entry {
37 + struct hashmap_entry ent;
38 + time_t whence;
39 + uint32_t cookie;
40 + const char *dir;
41 +};
42 +
43 +struct fsm_listen_data {
44 + int fd_inotify;
45 + enum shutdown_reason shutdown;
46 + struct hashmap watches;
47 + struct hashmap renames;
48 + struct hashmap revwatches;
49 +};
50 +
51 +static int watch_entry_cmp(const void *cmp_data UNUSED,
52 + const struct hashmap_entry *eptr,
53 + const struct hashmap_entry *entry_or_key,
54 + const void *keydata UNUSED)
55 +{
56 + const struct watch_entry *e1, *e2;
57 +
58 + e1 = container_of(eptr, const struct watch_entry, ent);
59 + e2 = container_of(entry_or_key, const struct watch_entry, ent);
60 + return e1->wd != e2->wd;
61 +}
62 +
63 +static int revwatches_entry_cmp(const void *cmp_data UNUSED,
64 + const struct hashmap_entry *eptr,
65 + const struct hashmap_entry *entry_or_key,
66 + const void *keydata UNUSED)
67 +{
68 + const struct watch_entry *e1, *e2;
69 +
70 + e1 = container_of(eptr, const struct watch_entry, ent);
71 + e2 = container_of(entry_or_key, const struct watch_entry, ent);
72 + return strcmp(e1->dir, e2->dir);
73 +}
74 +
75 +static int rename_entry_cmp(const void *cmp_data UNUSED,
76 + const struct hashmap_entry *eptr,
77 + const struct hashmap_entry *entry_or_key,
78 + const void *keydata UNUSED)
79 +{
80 + const struct rename_entry *e1, *e2;
81 +
82 + e1 = container_of(eptr, const struct rename_entry, ent);
83 + e2 = container_of(entry_or_key, const struct rename_entry, ent);
84 + return e1->cookie != e2->cookie;
85 +}
86 +
87 +/*
88 + * Register an inotify watch, add watch descriptor to path mapping
89 + * and the reverse mapping.
90 + */
91 +static int add_watch(const char *path, struct fsm_listen_data *data)
92 +{
93 + const char *interned = strintern(path);
94 + struct watch_entry *w1, *w2;
95 +
96 + /* add the inotify watch, don't allow watches to be modified */
97 + int wd = inotify_add_watch(data->fd_inotify, interned,
98 + (IN_ALL_EVENTS | IN_ONLYDIR | IN_MASK_CREATE)
99 + ^ IN_ACCESS ^ IN_CLOSE ^ IN_OPEN);
100 + if (wd < 0) {
101 + if (errno == ENOENT || errno == ENOTDIR)
102 + return 0; /* directory was deleted or is not a directory */
103 + if (errno == EEXIST)
104 + return 0; /* watch already exists, no action needed */
105 + if (errno == ENOSPC)
106 + return error(_("inotify watch limit reached; "
107 + "increase fs.inotify.max_user_watches"));
108 + return error_errno(_("inotify_add_watch('%s') failed"), interned);
109 + }
110 +
111 + /* add watch descriptor -> directory mapping */
112 + CALLOC_ARRAY(w1, 1);
113 + w1->wd = wd;
114 + w1->dir = interned;
115 + hashmap_entry_init(&w1->ent, memhash(&w1->wd, sizeof(int)));
116 + hashmap_add(&data->watches, &w1->ent);
117 +
118 + /* add directory -> watch descriptor mapping */
119 + CALLOC_ARRAY(w2, 1);
120 + w2->wd = wd;
121 + w2->dir = interned;
122 + hashmap_entry_init(&w2->ent, strhash(w2->dir));
123 + hashmap_add(&data->revwatches, &w2->ent);
124 +
125 + return 0;
126 +}
127 +
128 +/*
129 + * Remove the inotify watch, the watch descriptor to path mapping
130 + * and the reverse mapping.
131 + */
132 +static void remove_watch(struct watch_entry *w, struct fsm_listen_data *data)
133 +{
134 + struct watch_entry k1, k2, *w1, *w2;
135 +
136 + /* remove watch, ignore error if kernel already did it */
137 + if (inotify_rm_watch(data->fd_inotify, w->wd) && errno != EINVAL)
138 + error_errno(_("inotify_rm_watch() failed"));
139 +
140 + k1.wd = w->wd;
141 + hashmap_entry_init(&k1.ent, memhash(&k1.wd, sizeof(int)));
142 + w1 = hashmap_remove_entry(&data->watches, &k1, ent, NULL);
143 + if (!w1)
144 + BUG("double remove of watch for '%s'", w->dir);
145 +
146 + if (w1->cookie)
147 + BUG("removing watch for '%s' which has a pending rename", w1->dir);
148 +
149 + k2.dir = w->dir;
150 + hashmap_entry_init(&k2.ent, strhash(k2.dir));
151 + w2 = hashmap_remove_entry(&data->revwatches, &k2, ent, NULL);
152 + if (!w2)
153 + BUG("double remove of reverse watch for '%s'", w->dir);
154 +
155 + /* w1->dir and w2->dir are interned strings, we don't own them */
156 + free(w1);
157 + free(w2);
158 +}
159 +
160 +/*
161 + * Check for stale directory renames.
162 + *
163 + * https://man7.org/linux/man-pages/man7/inotify.7.html
164 + *
165 + * Allow for some small timeout to account for the fact that insertion of the
166 + * IN_MOVED_FROM+IN_MOVED_TO event pair is not atomic, and the possibility that
167 + * there may not be any IN_MOVED_TO event.
168 + *
169 + * If the IN_MOVED_TO event is not received within the timeout then events have
170 + * been missed and the monitor is in an inconsistent state with respect to the
171 + * filesystem.
172 + */
173 +static int check_stale_dir_renames(struct hashmap *renames, time_t max_age)
174 +{
175 + struct rename_entry *re;
176 + struct hashmap_iter iter;
177 +
178 + hashmap_for_each_entry(renames, &iter, re, ent) {
179 + if (re->whence <= max_age)
180 + return -1;
181 + }
182 + return 0;
183 +}
184 +
185 +/*
186 + * Track pending renames.
187 + *
188 + * Tracking is done via an event cookie to watch descriptor mapping.
189 + *
190 + * A rename is not complete until matching an IN_MOVED_TO event is received
191 + * for a corresponding IN_MOVED_FROM event.
192 + */
193 +static void add_dir_rename(uint32_t cookie, const char *path,
194 + struct fsm_listen_data *data)
195 +{
196 + struct watch_entry k, *w;
197 + struct rename_entry *re;
198 +
199 + /* lookup the watch descriptor for the given path */
200 + k.dir = path;
201 + hashmap_entry_init(&k.ent, strhash(path));
202 + w = hashmap_get_entry(&data->revwatches, &k, ent, NULL);
203 + if (!w) {
204 + /*
205 + * This can happen in rare cases where the directory was
206 + * moved before we had a chance to add a watch on it.
207 + * Just ignore this rename.
208 + */
209 + trace_printf_key(&trace_fsmonitor,
210 + "no watch found for rename from '%s'", path);
211 + return;
212 + }
213 + w->cookie = cookie;
214 +
215 + /* add the pending rename to match against later */
216 + CALLOC_ARRAY(re, 1);
217 + re->dir = w->dir;
218 + re->cookie = w->cookie;
219 + re->whence = time(NULL);
220 + hashmap_entry_init(&re->ent, memhash(&re->cookie, sizeof(uint32_t)));
221 + hashmap_add(&data->renames, &re->ent);
222 +}
223 +
224 +/*
225 + * Handle directory renames
226 + *
227 + * Once an IN_MOVED_TO event is received, lookup the rename tracking information
228 + * via the event cookie and use this information to update the watch.
229 + */
230 +static void rename_dir(uint32_t cookie, const char *path,
231 + struct fsm_listen_data *data)
232 +{
233 + struct rename_entry rek, *re;
234 + struct watch_entry k, *w;
235 +
236 + /* lookup a pending rename to match */
237 + rek.cookie = cookie;
238 + hashmap_entry_init(&rek.ent, memhash(&rek.cookie, sizeof(uint32_t)));
239 + re = hashmap_get_entry(&data->renames, &rek, ent, NULL);
240 + if (re) {
241 + k.dir = re->dir;
242 + hashmap_entry_init(&k.ent, strhash(k.dir));
243 + w = hashmap_get_entry(&data->revwatches, &k, ent, NULL);
244 + if (w) {
245 + w->cookie = 0; /* rename handled */
246 + remove_watch(w, data);
247 + if (add_watch(path, data))
248 + trace_printf_key(&trace_fsmonitor,
249 + "failed to add watch for renamed dir '%s'",
250 + path);
251 + } else {
252 + /* Directory was moved out of watch tree */
253 + trace_printf_key(&trace_fsmonitor,
254 + "no matching watch for rename to '%s'", path);
255 + }
256 + hashmap_remove_entry(&data->renames, &rek, ent, NULL);
257 + free(re);
258 + } else {
259 + /* Directory was moved from outside the watch tree */
260 + trace_printf_key(&trace_fsmonitor,
261 + "no matching cookie for rename to '%s'", path);
262 + }
263 +}
264 +
265 +/*
266 + * Recursively add watches to every directory under path
267 + */
268 +static int register_inotify(const char *path,
269 + struct fsmonitor_daemon_state *state,
270 + struct fsmonitor_batch *batch)
271 +{
272 + DIR *dir;
273 + const char *rel;
274 + struct strbuf current = STRBUF_INIT;
275 + struct dirent *de;
276 + struct stat fs;
277 + int ret = -1;
278 +
279 + dir = opendir(path);
280 + if (!dir) {
281 + if (errno == ENOENT || errno == ENOTDIR)
282 + return 0; /* directory was deleted */
283 + return error_errno(_("opendir('%s') failed"), path);
284 + }
285 +
286 + while ((de = readdir_skip_dot_and_dotdot(dir)) != NULL) {
287 + strbuf_reset(&current);
288 + strbuf_addf(&current, "%s/%s", path, de->d_name);
289 + if (lstat(current.buf, &fs)) {
290 + if (errno == ENOENT)
291 + continue; /* file was deleted */
292 + error_errno(_("lstat('%s') failed"), current.buf);
293 + goto failed;
294 + }
295 +
296 + /* recurse into directory */
297 + if (S_ISDIR(fs.st_mode)) {
298 + if (add_watch(current.buf, state->listen_data))
299 + goto failed;
300 + if (register_inotify(current.buf, state, batch))
301 + goto failed;
302 + } else if (batch) {
303 + rel = current.buf + state->path_worktree_watch.len + 1;
304 + trace_printf_key(&trace_fsmonitor, "explicitly adding '%s'", rel);
305 + fsmonitor_batch__add_path(batch, rel);
306 + }
307 + }
308 + ret = 0;
309 +
310 +failed:
311 + strbuf_release(&current);
312 + if (closedir(dir) < 0)
313 + return error_errno(_("closedir('%s') failed"), path);
314 + return ret;
315 +}
316 +
317 +static int em_rename_dir_from(uint32_t mask)
318 +{
319 + return ((mask & IN_ISDIR) && (mask & IN_MOVED_FROM));
320 +}
321 +
322 +static int em_rename_dir_to(uint32_t mask)
323 +{
324 + return ((mask & IN_ISDIR) && (mask & IN_MOVED_TO));
325 +}
326 +
327 +static int em_remove_watch(uint32_t mask)
328 +{
329 + return (mask & IN_DELETE_SELF);
330 +}
331 +
332 +static int em_dir_renamed(uint32_t mask)
333 +{
334 + return ((mask & IN_ISDIR) && (mask & IN_MOVE));
335 +}
336 +
337 +static int em_dir_created(uint32_t mask)
338 +{
339 + return ((mask & IN_ISDIR) && (mask & IN_CREATE));
340 +}
341 +
342 +static int em_dir_deleted(uint32_t mask)
343 +{
344 + return ((mask & IN_ISDIR) && (mask & IN_DELETE));
345 +}
346 +
347 +static int em_force_shutdown(uint32_t mask)
348 +{
349 + return (mask & IN_UNMOUNT) || (mask & IN_Q_OVERFLOW);
350 +}
351 +
352 +static int em_ignore(uint32_t mask)
353 +{
354 + return (mask & IN_IGNORED) || (mask & IN_MOVE_SELF);
355 +}
356 +
357 +static void log_mask_set(const char *path, uint32_t mask)
358 +{
359 + struct strbuf msg = STRBUF_INIT;
360 +
361 + if (mask & IN_ACCESS)
362 + strbuf_addstr(&msg, "IN_ACCESS|");
363 + if (mask & IN_MODIFY)
364 + strbuf_addstr(&msg, "IN_MODIFY|");
365 + if (mask & IN_ATTRIB)
366 + strbuf_addstr(&msg, "IN_ATTRIB|");
367 + if (mask & IN_CLOSE_WRITE)
368 + strbuf_addstr(&msg, "IN_CLOSE_WRITE|");
369 + if (mask & IN_CLOSE_NOWRITE)
370 + strbuf_addstr(&msg, "IN_CLOSE_NOWRITE|");
371 + if (mask & IN_OPEN)
372 + strbuf_addstr(&msg, "IN_OPEN|");
373 + if (mask & IN_MOVED_FROM)
374 + strbuf_addstr(&msg, "IN_MOVED_FROM|");
375 + if (mask & IN_MOVED_TO)
376 + strbuf_addstr(&msg, "IN_MOVED_TO|");
377 + if (mask & IN_CREATE)
378 + strbuf_addstr(&msg, "IN_CREATE|");
379 + if (mask & IN_DELETE)
380 + strbuf_addstr(&msg, "IN_DELETE|");
381 + if (mask & IN_DELETE_SELF)
382 + strbuf_addstr(&msg, "IN_DELETE_SELF|");
383 + if (mask & IN_MOVE_SELF)
384 + strbuf_addstr(&msg, "IN_MOVE_SELF|");
385 + if (mask & IN_UNMOUNT)
386 + strbuf_addstr(&msg, "IN_UNMOUNT|");
387 + if (mask & IN_Q_OVERFLOW)
388 + strbuf_addstr(&msg, "IN_Q_OVERFLOW|");
389 + if (mask & IN_IGNORED)
390 + strbuf_addstr(&msg, "IN_IGNORED|");
391 + if (mask & IN_ISDIR)
392 + strbuf_addstr(&msg, "IN_ISDIR|");
393 +
394 + strbuf_strip_suffix(&msg, "|");
395 +
396 + trace_printf_key(&trace_fsmonitor, "inotify_event: '%s', mask=%#8.8x %s",
397 + path, mask, msg.buf);
398 +
399 + strbuf_release(&msg);
400 +}
401 +
402 +int fsm_listen__ctor(struct fsmonitor_daemon_state *state)
403 +{
404 + int fd;
405 + int ret = 0;
406 + struct fsm_listen_data *data;
407 +
408 + CALLOC_ARRAY(data, 1);
409 + state->listen_data = data;
410 + state->listen_error_code = -1;
411 + data->fd_inotify = -1;
412 + data->shutdown = SHUTDOWN_ERROR;
413 +
414 + fd = inotify_init1(O_NONBLOCK);
415 + if (fd < 0) {
416 + FREE_AND_NULL(state->listen_data);
417 + return error_errno(_("inotify_init1() failed"));
418 + }
419 +
420 + data->fd_inotify = fd;
421 +
422 + hashmap_init(&data->watches, watch_entry_cmp, NULL, 0);
423 + hashmap_init(&data->renames, rename_entry_cmp, NULL, 0);
424 + hashmap_init(&data->revwatches, revwatches_entry_cmp, NULL, 0);
425 +
426 + if (add_watch(state->path_worktree_watch.buf, data))
427 + ret = -1;
428 + else if (register_inotify(state->path_worktree_watch.buf, state, NULL))
429 + ret = -1;
430 + else if (state->nr_paths_watching > 1) {
431 + if (add_watch(state->path_gitdir_watch.buf, data))
432 + ret = -1;
433 + else if (register_inotify(state->path_gitdir_watch.buf, state, NULL))
434 + ret = -1;
435 + }
436 +
437 + if (!ret) {
438 + state->listen_error_code = 0;
439 + data->shutdown = SHUTDOWN_CONTINUE;
440 + }
441 +
442 + return ret;
443 +}
444 +
445 +void fsm_listen__dtor(struct fsmonitor_daemon_state *state)
446 +{
447 + struct fsm_listen_data *data;
448 + struct hashmap_iter iter;
449 + struct watch_entry *w;
450 + struct watch_entry **to_remove;
451 + size_t nr_to_remove = 0, alloc_to_remove = 0;
452 + size_t i;
453 + int fd;
454 +
455 + if (!state || !state->listen_data)
456 + return;
457 +
458 + data = state->listen_data;
459 + fd = data->fd_inotify;
460 +
461 + /*
462 + * Collect all entries first, then remove them.
463 + * We can't modify the hashmap while iterating over it.
464 + */
465 + to_remove = NULL;
466 + hashmap_for_each_entry(&data->watches, &iter, w, ent) {
467 + ALLOC_GROW(to_remove, nr_to_remove + 1, alloc_to_remove);
468 + to_remove[nr_to_remove++] = w;
469 + }
470 +
471 + for (i = 0; i < nr_to_remove; i++) {
472 + to_remove[i]->cookie = 0; /* ignore any pending renames */
473 + remove_watch(to_remove[i], data);
474 + }
475 + free(to_remove);
476 +
477 + hashmap_clear(&data->watches);
478 +
479 + hashmap_clear(&data->revwatches); /* remove_watch freed the entries */
480 +
481 + hashmap_clear_and_free(&data->renames, struct rename_entry, ent);
482 +
483 + FREE_AND_NULL(state->listen_data);
484 +
485 + if (fd >= 0 && (close(fd) < 0))
486 + error_errno(_("closing inotify file descriptor failed"));
487 +}
488 +
489 +void fsm_listen__stop_async(struct fsmonitor_daemon_state *state)
490 +{
491 + if (state && state->listen_data &&
492 + state->listen_data->shutdown == SHUTDOWN_CONTINUE)
493 + state->listen_data->shutdown = SHUTDOWN_STOP;
494 +}
495 +
496 +/*
497 + * Process a single inotify event and queue for publication.
498 + */
499 +static int process_event(const char *path,
500 + const struct inotify_event *event,
501 + struct fsmonitor_batch **batch,
502 + struct string_list *cookie_list,
503 + struct fsmonitor_daemon_state *state)
504 +{
505 + const char *rel;
506 + const char *last_sep;
507 +
508 + switch (fsmonitor_classify_path_absolute(state, path)) {
509 + case IS_INSIDE_DOT_GIT_WITH_COOKIE_PREFIX:
510 + case IS_INSIDE_GITDIR_WITH_COOKIE_PREFIX:
511 + /* Use just the filename of the cookie file. */
512 + last_sep = find_last_dir_sep(path);
513 + string_list_append(cookie_list,
514 + last_sep ? last_sep + 1 : path);
515 + break;
516 + case IS_INSIDE_DOT_GIT:
517 + case IS_INSIDE_GITDIR:
518 + break;
519 + case IS_DOT_GIT:
520 + case IS_GITDIR:
521 + /*
522 + * If .git directory is deleted or renamed away,
523 + * we have to quit.
524 + */
525 + if (em_dir_deleted(event->mask)) {
526 + trace_printf_key(&trace_fsmonitor,
527 + "event: gitdir removed");
528 + state->listen_data->shutdown = SHUTDOWN_FORCE;
529 + goto done;
530 + }
531 +
532 + if (em_dir_renamed(event->mask)) {
533 + trace_printf_key(&trace_fsmonitor,
534 + "event: gitdir renamed");
535 + state->listen_data->shutdown = SHUTDOWN_FORCE;
536 + goto done;
537 + }
538 + break;
539 + case IS_WORKDIR_PATH:
540 + /* normal events in the working directory */
541 + if (trace_pass_fl(&trace_fsmonitor))
542 + log_mask_set(path, event->mask);
543 +
544 + if (!*batch)
545 + *batch = fsmonitor_batch__new();
546 +
547 + rel = path + state->path_worktree_watch.len + 1;
548 + fsmonitor_batch__add_path(*batch, rel);
549 +
550 + if (em_dir_deleted(event->mask))
551 + break;
552 +
553 + /* received IN_MOVE_FROM, add tracking for expected IN_MOVE_TO */
554 + if (em_rename_dir_from(event->mask))
555 + add_dir_rename(event->cookie, path, state->listen_data);
556 +
557 + /* received IN_MOVE_TO, update watch to reflect new path */
558 + if (em_rename_dir_to(event->mask)) {
559 + rename_dir(event->cookie, path, state->listen_data);
560 + if (register_inotify(path, state, *batch)) {
561 + state->listen_data->shutdown = SHUTDOWN_ERROR;
562 + goto done;
563 + }
564 + }
565 +
566 + if (em_dir_created(event->mask)) {
567 + if (add_watch(path, state->listen_data)) {
568 + state->listen_data->shutdown = SHUTDOWN_ERROR;
569 + goto done;
570 + }
571 + if (register_inotify(path, state, *batch)) {
572 + state->listen_data->shutdown = SHUTDOWN_ERROR;
573 + goto done;
574 + }
575 + }
576 + break;
577 + case IS_OUTSIDE_CONE:
578 + default:
579 + trace_printf_key(&trace_fsmonitor,
580 + "ignoring '%s'", path);
581 + break;
582 + }
583 + return 0;
584 +done:
585 + return -1;
586 +}
587 +
588 +/*
589 + * Read the inotify event stream and pre-process events before further
590 + * processing and eventual publishing.
591 + */
592 +static void handle_events(struct fsmonitor_daemon_state *state)
593 +{
594 + /* See https://man7.org/linux/man-pages/man7/inotify.7.html */
595 + char buf[4096]
596 + __attribute__ ((aligned(__alignof__(struct inotify_event))));
597 +
598 + struct hashmap *watches = &state->listen_data->watches;
599 + struct fsmonitor_batch *batch = NULL;
600 + struct string_list cookie_list = STRING_LIST_INIT_DUP;
601 + struct watch_entry k, *w;
602 + struct strbuf path = STRBUF_INIT;
603 + const struct inotify_event *event;
604 + int fd = state->listen_data->fd_inotify;
605 + ssize_t len;
606 + char *ptr, *p;
607 +
608 + for (;;) {
609 + len = read(fd, buf, sizeof(buf));
610 + if (len == -1) {
611 + if (errno == EAGAIN || errno == EINTR)
612 + goto done;
613 + error_errno(_("reading inotify message stream failed"));
614 + state->listen_data->shutdown = SHUTDOWN_ERROR;
615 + goto done;
616 + }
617 +
618 + /* nothing to read */
619 + if (len == 0)
620 + goto done;
621 +
622 + /* Loop over all events in the buffer. */
623 + for (ptr = buf; ptr < buf + len;
624 + ptr += sizeof(struct inotify_event) + event->len) {
625 +
626 + event = (const struct inotify_event *)ptr;
627 +
628 + if (em_ignore(event->mask))
629 + continue;
630 +
631 + /* File system was unmounted or event queue overflowed */
632 + if (em_force_shutdown(event->mask)) {
633 + if (trace_pass_fl(&trace_fsmonitor))
634 + log_mask_set("forcing shutdown", event->mask);
635 + state->listen_data->shutdown = SHUTDOWN_FORCE;
636 + goto done;
637 + }
638 +
639 + k.wd = event->wd;
640 + hashmap_entry_init(&k.ent, memhash(&k.wd, sizeof(int)));
641 +
642 + w = hashmap_get_entry(watches, &k, ent, NULL);
643 + if (!w) {
644 + /* Watch was removed, skip event */
645 + continue;
646 + }
647 +
648 + /* directory watch was removed */
649 + if (em_remove_watch(event->mask)) {
650 + remove_watch(w, state->listen_data);
651 + continue;
652 + }
653 +
654 + strbuf_reset(&path);
655 + strbuf_addf(&path, "%s/%s", w->dir, event->name);
656 +
657 + p = fsmonitor__resolve_alias(path.buf, &state->alias);
658 + if (!p)
659 + p = strbuf_detach(&path, NULL);
660 +
661 + if (process_event(p, event, &batch, &cookie_list, state)) {
662 + free(p);
663 + goto done;
664 + }
665 + free(p);
666 + }
667 + strbuf_reset(&path);
668 + fsmonitor_publish(state, batch, &cookie_list);
669 + string_list_clear(&cookie_list, 0);
670 + batch = NULL;
671 + }
672 +done:
673 + strbuf_release(&path);
674 + fsmonitor_batch__free_list(batch);
675 + string_list_clear(&cookie_list, 0);
676 +}
677 +
678 +/*
679 + * Non-blocking read of the inotify events stream. The inotify fd is polled
680 + * frequently to help minimize the number of queue overflows.
681 + */
682 +void fsm_listen__loop(struct fsmonitor_daemon_state *state)
683 +{
684 + int poll_num;
685 + /*
686 + * Interval in seconds between checks for stale directory renames.
687 + * A directory rename that is not completed within this window
688 + * (i.e. no matching IN_MOVED_TO for an IN_MOVED_FROM) indicates
689 + * missed events, forcing a shutdown.
690 + */
691 + const int interval = 1;
692 + time_t checked = time(NULL);
693 + struct pollfd fds[1];
694 +
695 + fds[0].fd = state->listen_data->fd_inotify;
696 + fds[0].events = POLLIN;
697 +
698 + /*
699 + * Our fs event listener is now running, so it's safe to start
700 + * serving client requests.
701 + */
702 + ipc_server_start_async(state->ipc_server_data);
703 +
704 + for (;;) {
705 + switch (state->listen_data->shutdown) {
706 + case SHUTDOWN_CONTINUE:
707 + poll_num = poll(fds, 1, 50);
708 + if (poll_num == -1) {
709 + if (errno == EINTR)
710 + continue;
711 + error_errno(_("polling inotify message stream failed"));
712 + state->listen_data->shutdown = SHUTDOWN_ERROR;
713 + continue;
714 + }
715 +
716 + if ((time(NULL) - checked) >= interval) {
717 + checked = time(NULL);
718 + if (check_stale_dir_renames(&state->listen_data->renames,
719 + checked - interval)) {
720 + trace_printf_key(&trace_fsmonitor,
721 + "missed IN_MOVED_TO events, forcing shutdown");
722 + state->listen_data->shutdown = SHUTDOWN_FORCE;
723 + continue;
724 + }
725 + }
726 +
727 + if (poll_num > 0 && (fds[0].revents & POLLIN))
728 + handle_events(state);
729 +
730 + continue;
731 + case SHUTDOWN_ERROR:
732 + state->listen_error_code = -1;
733 + ipc_server_stop_async(state->ipc_server_data);
734 + break;
735 + case SHUTDOWN_FORCE:
736 + state->listen_error_code = 0;
737 + ipc_server_stop_async(state->ipc_server_data);
738 + break;
739 + case SHUTDOWN_STOP:
740 + default:
741 + state->listen_error_code = 0;
742 + break;
743 + }
744 + return;
745 + }
746 +}
compat/fsmonitor/fsm-path-utils-linux.c new
+217
@@ -0,0 +1,217 @@
1 +#include "git-compat-util.h"
2 +#include "fsmonitor-ll.h"
3 +#include "fsmonitor-path-utils.h"
4 +#include "gettext.h"
5 +#include "trace.h"
6 +
7 +#include <sys/statfs.h>
8 +
9 +#ifdef HAVE_LINUX_MAGIC_H
10 +#include <linux/magic.h>
11 +#endif
12 +
13 +/*
14 + * Filesystem magic numbers for remote filesystems.
15 + * Defined here if not available in linux/magic.h.
16 + */
17 +#ifndef CIFS_SUPER_MAGIC
18 +#define CIFS_SUPER_MAGIC 0xff534d42
19 +#endif
20 +#ifndef SMB_SUPER_MAGIC
21 +#define SMB_SUPER_MAGIC 0x517b
22 +#endif
23 +#ifndef SMB2_SUPER_MAGIC
24 +#define SMB2_SUPER_MAGIC 0xfe534d42
25 +#endif
26 +#ifndef NFS_SUPER_MAGIC
27 +#define NFS_SUPER_MAGIC 0x6969
28 +#endif
29 +#ifndef AFS_SUPER_MAGIC
30 +#define AFS_SUPER_MAGIC 0x5346414f
31 +#endif
32 +#ifndef CODA_SUPER_MAGIC
33 +#define CODA_SUPER_MAGIC 0x73757245
34 +#endif
35 +#ifndef FUSE_SUPER_MAGIC
36 +#define FUSE_SUPER_MAGIC 0x65735546
37 +#endif
38 +
39 +/*
40 + * Check if filesystem type is a remote filesystem.
41 + */
42 +static int is_remote_fs(unsigned long f_type)
43 +{
44 + switch (f_type) {
45 + case CIFS_SUPER_MAGIC:
46 + case SMB_SUPER_MAGIC:
47 + case SMB2_SUPER_MAGIC:
48 + case NFS_SUPER_MAGIC:
49 + case AFS_SUPER_MAGIC:
50 + case CODA_SUPER_MAGIC:
51 + case FUSE_SUPER_MAGIC:
52 + return 1;
53 + default:
54 + return 0;
55 + }
56 +}
57 +
58 +/*
59 + * Map filesystem magic numbers to human-readable names as a fallback
60 + * when /proc/mounts is unavailable. This only covers the remote and
61 + * special filesystems in is_remote_fs() above; local filesystems are
62 + * never flagged as incompatible, so we do not need their names here.
63 + */
64 +static const char *get_fs_typename(unsigned long f_type)
65 +{
66 + switch (f_type) {
67 + case CIFS_SUPER_MAGIC:
68 + return "cifs";
69 + case SMB_SUPER_MAGIC:
70 + return "smb";
71 + case SMB2_SUPER_MAGIC:
72 + return "smb2";
73 + case NFS_SUPER_MAGIC:
74 + return "nfs";
75 + case AFS_SUPER_MAGIC:
76 + return "afs";
77 + case CODA_SUPER_MAGIC:
78 + return "coda";
79 + case FUSE_SUPER_MAGIC:
80 + return "fuse";
81 + default:
82 + return "unknown";
83 + }
84 +}
85 +
86 +/*
87 + * Find the mount point for a given path by reading /proc/mounts.
88 + *
89 + * statfs(2) gives us f_type (the magic number) but not the human-readable
90 + * filesystem type string. We scan /proc/mounts to find the mount entry
91 + * whose path is the longest prefix of ours and whose f_fsid matches,
92 + * which gives us the fstype string (e.g. "nfs", "ext4") for logging.
93 + */
94 +static char *find_mount(const char *path, const struct statfs *path_fs)
95 +{
96 + FILE *fp;
97 + struct strbuf line = STRBUF_INIT;
98 + struct strbuf match = STRBUF_INIT;
99 + struct strbuf fstype = STRBUF_INIT;
100 + char *result = NULL;
101 +
102 + fp = fopen("/proc/mounts", "r");
103 + if (!fp)
104 + return NULL;
105 +
106 + while (strbuf_getline(&line, fp) != EOF) {
107 + char *fields[6];
108 + char *p = line.buf;
109 + int i;
110 +
111 + /* Parse mount entry: device mountpoint fstype options dump pass */
112 + for (i = 0; i < 6 && p; i++) {
113 + fields[i] = p;
114 + p = strchr(p, ' ');
115 + if (p)
116 + *p++ = '\0';
117 + }
118 +
119 + if (i >= 3) {
120 + const char *mountpoint = fields[1];
121 + const char *type = fields[2];
122 + struct statfs mount_fs;
123 +
124 + /* Check if this mount point is a prefix of our path */
125 + if (starts_with(path, mountpoint) &&
126 + (path[strlen(mountpoint)] == '/' ||
127 + path[strlen(mountpoint)] == '\0')) {
128 + /* Check if filesystem ID matches */
129 + if (statfs(mountpoint, &mount_fs) == 0 &&
130 + !memcmp(&mount_fs.f_fsid, &path_fs->f_fsid,
131 + sizeof(mount_fs.f_fsid))) {
132 + /* Keep the longest matching mount point */
133 + if (strlen(mountpoint) > match.len) {
134 + strbuf_reset(&match);
135 + strbuf_addstr(&match, mountpoint);
136 + strbuf_reset(&fstype);
137 + strbuf_addstr(&fstype, type);
138 + }
139 + }
140 + }
141 + }
142 + }
143 +
144 + fclose(fp);
145 + strbuf_release(&line);
146 + strbuf_release(&match);
147 +
148 + if (fstype.len)
149 + result = strbuf_detach(&fstype, NULL);
150 + else
151 + strbuf_release(&fstype);
152 +
153 + return result;
154 +}
155 +
156 +int fsmonitor__get_fs_info(const char *path, struct fs_info *fs_info)
157 +{
158 + struct statfs fs;
159 +
160 + if (statfs(path, &fs) == -1) {
161 + int saved_errno = errno;
162 + trace_printf_key(&trace_fsmonitor, "statfs('%s') failed: %s",
163 + path, strerror(saved_errno));
164 + errno = saved_errno;
165 + return -1;
166 + }
167 +
168 + trace_printf_key(&trace_fsmonitor,
169 + "statfs('%s') [type 0x%08lx]",
170 + path, (unsigned long)fs.f_type);
171 +
172 + fs_info->is_remote = is_remote_fs(fs.f_type);
173 +
174 + /*
175 + * Try to get filesystem type from /proc/mounts for a more
176 + * descriptive name.
177 + */
178 + fs_info->typename = find_mount(path, &fs);
179 + if (!fs_info->typename)
180 + fs_info->typename = xstrdup(get_fs_typename(fs.f_type));
181 +
182 + trace_printf_key(&trace_fsmonitor,
183 + "'%s' is_remote: %d, typename: %s",
184 + path, fs_info->is_remote, fs_info->typename);
185 +
186 + return 0;
187 +}
188 +
189 +int fsmonitor__is_fs_remote(const char *path)
190 +{
191 + struct fs_info fs;
192 +
193 + if (fsmonitor__get_fs_info(path, &fs))
194 + return -1;
195 +
196 + free(fs.typename);
197 +
198 + return fs.is_remote;
199 +}
200 +
201 +/*
202 + * No-op for Linux - we don't have firmlinks like macOS.
203 + */
204 +int fsmonitor__get_alias(const char *path UNUSED,
205 + struct alias_info *info UNUSED)
206 +{
207 + return 0;
208 +}
209 +
210 +/*
211 + * No-op for Linux - we don't have firmlinks like macOS.
212 + */
213 +char *fsmonitor__resolve_alias(const char *path UNUSED,
214 + const struct alias_info *info UNUSED)
215 +{
216 + return NULL;
217 +}
config.mak.uname
+10
@@ -68,6 +68,16 @@ ifeq ($(uname_S),Linux)
68 BASIC_CFLAGS += -std=c99
69 endif
70 LINK_FUZZ_PROGRAMS = YesPlease
71 +
72 + # The builtin FSMonitor on Linux builds upon Simple-IPC. Both require
73 + # Unix domain sockets and PThreads.
74 + ifndef NO_PTHREADS
75 + ifndef NO_UNIX_SOCKETS
76 + FSMONITOR_DAEMON_BACKEND = linux
77 + FSMONITOR_OS_SETTINGS = unix
78 + BASIC_CFLAGS += -DHAVE_LINUX_MAGIC_H
79 + endif
80 + endif
81 endif
82 ifeq ($(uname_S),GNU/kFreeBSD)
83 HAVE_ALLOCA_H = YesPlease
contrib/buildsystems/CMakeLists.txt
+6 -2
@@ -296,6 +296,10 @@ if(SUPPORTS_SIMPLE_IPC)
296 elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
297 set(FSMONITOR_DAEMON_BACKEND "darwin")
298 set(FSMONITOR_OS_SETTINGS "unix")
299 + elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux")
300 + set(FSMONITOR_DAEMON_BACKEND "linux")
301 + set(FSMONITOR_OS_SETTINGS "unix")
302 + add_compile_definitions(HAVE_LINUX_MAGIC_H)
303 endif()
304
305 if(FSMONITOR_DAEMON_BACKEND)
@@ -1149,8 +1153,8 @@ endif()
1153 file(STRINGS ${CMAKE_SOURCE_DIR}/GIT-BUILD-OPTIONS.in git_build_options NEWLINE_CONSUME)
1154 string(REPLACE "@BROKEN_PATH_FIX@" "" git_build_options "${git_build_options}")
1155 string(REPLACE "@DIFF@" "'${DIFF}'" git_build_options "${git_build_options}")
1152 -string(REPLACE "@FSMONITOR_DAEMON_BACKEND@" "win32" git_build_options "${git_build_options}")
1153 -string(REPLACE "@FSMONITOR_OS_SETTINGS@" "win32" git_build_options "${git_build_options}")
1156 +string(REPLACE "@FSMONITOR_DAEMON_BACKEND@" "${FSMONITOR_DAEMON_BACKEND}" git_build_options "${git_build_options}")
1157 +string(REPLACE "@FSMONITOR_OS_SETTINGS@" "${FSMONITOR_OS_SETTINGS}" git_build_options "${git_build_options}")
1158 string(REPLACE "@GITWEBDIR@" "'${GITWEBDIR}'" git_build_options "${git_build_options}")
1159 string(REPLACE "@GIT_INTEROP_MAKE_OPTS@" "" git_build_options "${git_build_options}")
1160 string(REPLACE "@GIT_PERF_LARGE_REPO@" "" git_build_options "${git_build_options}")
meson.build
+4
@@ -1324,6 +1324,10 @@ fsmonitor_os = ''
1324 if host_machine.system() == 'windows'
1325 fsmonitor_backend = 'win32'
1326 fsmonitor_os = 'win32'
1327 +elif host_machine.system() == 'linux' and threads.found() and compiler.has_header('linux/magic.h')
1328 + fsmonitor_backend = 'linux'
1329 + fsmonitor_os = 'unix'
1330 + libgit_c_args += '-DHAVE_LINUX_MAGIC_H'
1331 elif host_machine.system() == 'darwin'
1332 fsmonitor_backend = 'darwin'
1333 fsmonitor_os = 'unix'