compat/fsmonitor/fsm-listen-win32: implement FSMonitor backend on Windows

Teach the win32 backend to register a watch on the working tree root directory (recursively). Also watch the <gitdir> if it is not inside the working tree. And to collect path change notifications into batches and publish. Signed-off-by: Jeff Hostetler <jeffhost@microsoft.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Jeff Hostetler committed Mar 25, 2022 at 18:02 UTC 1448edfb5115c947e377d650e02f8a3ab7fa7b93
1 file changed +565
compat/fsmonitor/fsm-listen-win32.c
+565
@@ -2,20 +2,585 @@
2 #include "config.h"
3 #include "fsmonitor.h"
4 #include "fsm-listen.h"
5 +#include "fsmonitor--daemon.h"
6 +
7 +/*
8 + * The documentation of ReadDirectoryChangesW() states that the maximum
9 + * buffer size is 64K when the monitored directory is remote.
10 + *
11 + * Larger buffers may be used when the monitored directory is local and
12 + * will help us receive events faster from the kernel and avoid dropped
13 + * events.
14 + *
15 + * So we try to use a very large buffer and silently fallback to 64K if
16 + * we get an error.
17 + */
18 +#define MAX_RDCW_BUF_FALLBACK (65536)
19 +#define MAX_RDCW_BUF (65536 * 8)
20 +
21 +struct one_watch
22 +{
23 + char buffer[MAX_RDCW_BUF];
24 + DWORD buf_len;
25 + DWORD count;
26 +
27 + struct strbuf path;
28 + HANDLE hDir;
29 + HANDLE hEvent;
30 + OVERLAPPED overlapped;
31 +
32 + /*
33 + * Is there an active ReadDirectoryChangesW() call pending. If so, we
34 + * need to later call GetOverlappedResult() and possibly CancelIoEx().
35 + */
36 + BOOL is_active;
37 +};
38 +
39 +struct fsmonitor_daemon_backend_data
40 +{
41 + struct one_watch *watch_worktree;
42 + struct one_watch *watch_gitdir;
43 +
44 + HANDLE hEventShutdown;
45 +
46 + HANDLE hListener[3]; /* we don't own these handles */
47 +#define LISTENER_SHUTDOWN 0
48 +#define LISTENER_HAVE_DATA_WORKTREE 1
49 +#define LISTENER_HAVE_DATA_GITDIR 2
50 + int nr_listener_handles;
51 +};
52 +
53 +/*
54 + * Convert the WCHAR path from the notification into UTF8 and
55 + * then normalize it.
56 + */
57 +static int normalize_path_in_utf8(FILE_NOTIFY_INFORMATION *info,
58 + struct strbuf *normalized_path)
59 +{
60 + int reserve;
61 + int len = 0;
62 +
63 + strbuf_reset(normalized_path);
64 + if (!info->FileNameLength)
65 + goto normalize;
66 +
67 + /*
68 + * Pre-reserve enough space in the UTF8 buffer for
69 + * each Unicode WCHAR character to be mapped into a
70 + * sequence of 2 UTF8 characters. That should let us
71 + * avoid ERROR_INSUFFICIENT_BUFFER 99.9+% of the time.
72 + */
73 + reserve = info->FileNameLength + 1;
74 + strbuf_grow(normalized_path, reserve);
75 +
76 + for (;;) {
77 + len = WideCharToMultiByte(CP_UTF8, 0, info->FileName,
78 + info->FileNameLength / sizeof(WCHAR),
79 + normalized_path->buf,
80 + strbuf_avail(normalized_path) - 1,
81 + NULL, NULL);
82 + if (len > 0)
83 + goto normalize;
84 + if (GetLastError() != ERROR_INSUFFICIENT_BUFFER) {
85 + error(_("[GLE %ld] could not convert path to UTF-8: '%.*ls'"),
86 + GetLastError(),
87 + (int)(info->FileNameLength / sizeof(WCHAR)),
88 + info->FileName);
89 + return -1;
90 + }
91 +
92 + strbuf_grow(normalized_path,
93 + strbuf_avail(normalized_path) + reserve);
94 + }
95 +
96 +normalize:
97 + strbuf_setlen(normalized_path, len);
98 + return strbuf_normalize_path(normalized_path);
99 +}
100
101 void fsm_listen__stop_async(struct fsmonitor_daemon_state *state)
102 {
103 + SetEvent(state->backend_data->hListener[LISTENER_SHUTDOWN]);
104 +}
105 +
106 +static struct one_watch *create_watch(struct fsmonitor_daemon_state *state,
107 + const char *path)
108 +{
109 + struct one_watch *watch = NULL;
110 + DWORD desired_access = FILE_LIST_DIRECTORY;
111 + DWORD share_mode =
112 + FILE_SHARE_WRITE | FILE_SHARE_READ | FILE_SHARE_DELETE;
113 + HANDLE hDir;
114 + wchar_t wpath[MAX_PATH];
115 +
116 + if (xutftowcs_path(wpath, path) < 0) {
117 + error(_("could not convert to wide characters: '%s'"), path);
118 + return NULL;
119 + }
120 +
121 + hDir = CreateFileW(wpath,
122 + desired_access, share_mode, NULL, OPEN_EXISTING,
123 + FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED,
124 + NULL);
125 + if (hDir == INVALID_HANDLE_VALUE) {
126 + error(_("[GLE %ld] could not watch '%s'"),
127 + GetLastError(), path);
128 + return NULL;
129 + }
130 +
131 + CALLOC_ARRAY(watch, 1);
132 +
133 + watch->buf_len = sizeof(watch->buffer); /* assume full MAX_RDCW_BUF */
134 +
135 + strbuf_init(&watch->path, 0);
136 + strbuf_addstr(&watch->path, path);
137 +
138 + watch->hDir = hDir;
139 + watch->hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
140 +
141 + return watch;
142 +}
143 +
144 +static void destroy_watch(struct one_watch *watch)
145 +{
146 + if (!watch)
147 + return;
148 +
149 + strbuf_release(&watch->path);
150 + if (watch->hDir != INVALID_HANDLE_VALUE)
151 + CloseHandle(watch->hDir);
152 + if (watch->hEvent != INVALID_HANDLE_VALUE)
153 + CloseHandle(watch->hEvent);
154 +
155 + free(watch);
156 +}
157 +
158 +static int start_rdcw_watch(struct fsmonitor_daemon_backend_data *data,
159 + struct one_watch *watch)
160 +{
161 + DWORD dwNotifyFilter =
162 + FILE_NOTIFY_CHANGE_FILE_NAME |
163 + FILE_NOTIFY_CHANGE_DIR_NAME |
164 + FILE_NOTIFY_CHANGE_ATTRIBUTES |
165 + FILE_NOTIFY_CHANGE_SIZE |
166 + FILE_NOTIFY_CHANGE_LAST_WRITE |
167 + FILE_NOTIFY_CHANGE_CREATION;
168 +
169 + ResetEvent(watch->hEvent);
170 +
171 + memset(&watch->overlapped, 0, sizeof(watch->overlapped));
172 + watch->overlapped.hEvent = watch->hEvent;
173 +
174 + /*
175 + * Queue an async call using Overlapped IO. This returns immediately.
176 + * Our event handle will be signalled when the real result is available.
177 + *
178 + * The return value here just means that we successfully queued it.
179 + * We won't know if the Read...() actually produces data until later.
180 + */
181 + watch->is_active = ReadDirectoryChangesW(
182 + watch->hDir, watch->buffer, watch->buf_len, TRUE,
183 + dwNotifyFilter, &watch->count, &watch->overlapped, NULL);
184 +
185 + if (watch->is_active)
186 + return 0;
187 +
188 + error(_("ReadDirectoryChangedW failed on '%s' [GLE %ld]"),
189 + watch->path.buf, GetLastError());
190 + return -1;
191 +}
192 +
193 +static int recv_rdcw_watch(struct one_watch *watch)
194 +{
195 + DWORD gle;
196 +
197 + watch->is_active = FALSE;
198 +
199 + /*
200 + * The overlapped result is ready. If the Read...() was successful
201 + * we finally receive the actual result into our buffer.
202 + */
203 + if (GetOverlappedResult(watch->hDir, &watch->overlapped, &watch->count,
204 + TRUE))
205 + return 0;
206 +
207 + gle = GetLastError();
208 + if (gle == ERROR_INVALID_PARAMETER &&
209 + /*
210 + * The kernel throws an invalid parameter error when our
211 + * buffer is too big and we are pointed at a remote
212 + * directory (and possibly for other reasons). Quietly
213 + * set it down and try again.
214 + *
215 + * See note about MAX_RDCW_BUF at the top.
216 + */
217 + watch->buf_len > MAX_RDCW_BUF_FALLBACK) {
218 + watch->buf_len = MAX_RDCW_BUF_FALLBACK;
219 + return -2;
220 + }
221 +
222 + /*
223 + * NEEDSWORK: If an external <gitdir> is deleted, the above
224 + * returns an error. I'm not sure that there's anything that
225 + * we can do here other than failing -- the <worktree>/.git
226 + * link file would be broken anyway. We might try to check
227 + * for that and return a better error message, but I'm not
228 + * sure it is worth it.
229 + */
230 +
231 + error(_("GetOverlappedResult failed on '%s' [GLE %ld]"),
232 + watch->path.buf, gle);
233 + return -1;
234 +}
235 +
236 +static void cancel_rdcw_watch(struct one_watch *watch)
237 +{
238 + DWORD count;
239 +
240 + if (!watch || !watch->is_active)
241 + return;
242 +
243 + /*
244 + * The calls to ReadDirectoryChangesW() and GetOverlappedResult()
245 + * form a "pair" (my term) where we queue an IO and promise to
246 + * hang around and wait for the kernel to give us the result.
247 + *
248 + * If for some reason after we queue the IO, we have to quit
249 + * or otherwise not stick around for the second half, we must
250 + * tell the kernel to abort the IO. This prevents the kernel
251 + * from writing to our buffer and/or signalling our event
252 + * after we free them.
253 + *
254 + * (Ask me how much fun it was to track that one down).
255 + */
256 + CancelIoEx(watch->hDir, &watch->overlapped);
257 + GetOverlappedResult(watch->hDir, &watch->overlapped, &count, TRUE);
258 + watch->is_active = FALSE;
259 +}
260 +
261 +/*
262 + * Process filesystem events that happen anywhere (recursively) under the
263 + * <worktree> root directory. For a normal working directory, this includes
264 + * both version controlled files and the contents of the .git/ directory.
265 + *
266 + * If <worktree>/.git is a file, then we only see events for the file
267 + * itself.
268 + */
269 +static int process_worktree_events(struct fsmonitor_daemon_state *state)
270 +{
271 + struct fsmonitor_daemon_backend_data *data = state->backend_data;
272 + struct one_watch *watch = data->watch_worktree;
273 + struct strbuf path = STRBUF_INIT;
274 + struct string_list cookie_list = STRING_LIST_INIT_DUP;
275 + struct fsmonitor_batch *batch = NULL;
276 + const char *p = watch->buffer;
277 +
278 + /*
279 + * If the kernel gets more events than will fit in the kernel
280 + * buffer associated with our RDCW handle, it drops them and
281 + * returns a count of zero.
282 + *
283 + * Yes, the call returns WITHOUT error and with length zero.
284 + * This is the documented behavior. (My testing has confirmed
285 + * that it also sets the last error to ERROR_NOTIFY_ENUM_DIR,
286 + * but we do not rely on that since the function did not
287 + * return an error and it is not documented.)
288 + *
289 + * (The "overflow" case is not ambiguous with the "no data" case
290 + * because we did an INFINITE wait.)
291 + *
292 + * This means we have a gap in coverage. Tell the daemon layer
293 + * to resync.
294 + */
295 + if (!watch->count) {
296 + trace2_data_string("fsmonitor", NULL, "fsm-listen/kernel",
297 + "overflow");
298 + fsmonitor_force_resync(state);
299 + return LISTENER_HAVE_DATA_WORKTREE;
300 + }
301 +
302 + /*
303 + * On Windows, `info` contains an "array" of paths that are
304 + * relative to the root of whichever directory handle received
305 + * the event.
306 + */
307 + for (;;) {
308 + FILE_NOTIFY_INFORMATION *info = (void *)p;
309 + const char *slash;
310 + enum fsmonitor_path_type t;
311 +
312 + strbuf_reset(&path);
313 + if (normalize_path_in_utf8(info, &path) == -1)
314 + goto skip_this_path;
315 +
316 + t = fsmonitor_classify_path_workdir_relative(path.buf);
317 +
318 + switch (t) {
319 + case IS_INSIDE_DOT_GIT_WITH_COOKIE_PREFIX:
320 + /* special case cookie files within .git */
321 +
322 + /* Use just the filename of the cookie file. */
323 + slash = find_last_dir_sep(path.buf);
324 + string_list_append(&cookie_list,
325 + slash ? slash + 1 : path.buf);
326 + break;
327 +
328 + case IS_INSIDE_DOT_GIT:
329 + /* ignore everything inside of "<worktree>/.git/" */
330 + break;
331 +
332 + case IS_DOT_GIT:
333 + /* "<worktree>/.git" was deleted (or renamed away) */
334 + if ((info->Action == FILE_ACTION_REMOVED) ||
335 + (info->Action == FILE_ACTION_RENAMED_OLD_NAME)) {
336 + trace2_data_string("fsmonitor", NULL,
337 + "fsm-listen/dotgit",
338 + "removed");
339 + goto force_shutdown;
340 + }
341 + break;
342 +
343 + case IS_WORKDIR_PATH:
344 + /* queue normal pathname */
345 + if (!batch)
346 + batch = fsmonitor_batch__new();
347 + fsmonitor_batch__add_path(batch, path.buf);
348 + break;
349 +
350 + case IS_GITDIR:
351 + case IS_INSIDE_GITDIR:
352 + case IS_INSIDE_GITDIR_WITH_COOKIE_PREFIX:
353 + default:
354 + BUG("unexpected path classification '%d' for '%s'",
355 + t, path.buf);
356 + }
357 +
358 +skip_this_path:
359 + if (!info->NextEntryOffset)
360 + break;
361 + p += info->NextEntryOffset;
362 + }
363 +
364 + fsmonitor_publish(state, batch, &cookie_list);
365 + batch = NULL;
366 + string_list_clear(&cookie_list, 0);
367 + strbuf_release(&path);
368 + return LISTENER_HAVE_DATA_WORKTREE;
369 +
370 +force_shutdown:
371 + fsmonitor_batch__free_list(batch);
372 + string_list_clear(&cookie_list, 0);
373 + strbuf_release(&path);
374 + return LISTENER_SHUTDOWN;
375 +}
376 +
377 +/*
378 + * Process filesystem events that happened anywhere (recursively) under the
379 + * external <gitdir> (such as non-primary worktrees or submodules).
380 + * We only care about cookie files that our client threads created here.
381 + *
382 + * Note that we DO NOT get filesystem events on the external <gitdir>
383 + * itself (it is not inside something that we are watching). In particular,
384 + * we do not get an event if the external <gitdir> is deleted.
385 + */
386 +static int process_gitdir_events(struct fsmonitor_daemon_state *state)
387 +{
388 + struct fsmonitor_daemon_backend_data *data = state->backend_data;
389 + struct one_watch *watch = data->watch_gitdir;
390 + struct strbuf path = STRBUF_INIT;
391 + struct string_list cookie_list = STRING_LIST_INIT_DUP;
392 + const char *p = watch->buffer;
393 +
394 + if (!watch->count) {
395 + trace2_data_string("fsmonitor", NULL, "fsm-listen/kernel",
396 + "overflow");
397 + fsmonitor_force_resync(state);
398 + return LISTENER_HAVE_DATA_GITDIR;
399 + }
400 +
401 + for (;;) {
402 + FILE_NOTIFY_INFORMATION *info = (void *)p;
403 + const char *slash;
404 + enum fsmonitor_path_type t;
405 +
406 + strbuf_reset(&path);
407 + if (normalize_path_in_utf8(info, &path) == -1)
408 + goto skip_this_path;
409 +
410 + t = fsmonitor_classify_path_gitdir_relative(path.buf);
411 +
412 + switch (t) {
413 + case IS_INSIDE_GITDIR_WITH_COOKIE_PREFIX:
414 + /* special case cookie files within gitdir */
415 +
416 + /* Use just the filename of the cookie file. */
417 + slash = find_last_dir_sep(path.buf);
418 + string_list_append(&cookie_list,
419 + slash ? slash + 1 : path.buf);
420 + break;
421 +
422 + case IS_INSIDE_GITDIR:
423 + goto skip_this_path;
424 +
425 + default:
426 + BUG("unexpected path classification '%d' for '%s'",
427 + t, path.buf);
428 + }
429 +
430 +skip_this_path:
431 + if (!info->NextEntryOffset)
432 + break;
433 + p += info->NextEntryOffset;
434 + }
435 +
436 + fsmonitor_publish(state, NULL, &cookie_list);
437 + string_list_clear(&cookie_list, 0);
438 + strbuf_release(&path);
439 + return LISTENER_HAVE_DATA_GITDIR;
440 }
441
442 void fsm_listen__loop(struct fsmonitor_daemon_state *state)
443 {
444 + struct fsmonitor_daemon_backend_data *data = state->backend_data;
445 + DWORD dwWait;
446 + int result;
447 +
448 + state->error_code = 0;
449 +
450 + if (start_rdcw_watch(data, data->watch_worktree) == -1)
451 + goto force_error_stop;
452 +
453 + if (data->watch_gitdir &&
454 + start_rdcw_watch(data, data->watch_gitdir) == -1)
455 + goto force_error_stop;
456 +
457 + for (;;) {
458 + dwWait = WaitForMultipleObjects(data->nr_listener_handles,
459 + data->hListener,
460 + FALSE, INFINITE);
461 +
462 + if (dwWait == WAIT_OBJECT_0 + LISTENER_HAVE_DATA_WORKTREE) {
463 + result = recv_rdcw_watch(data->watch_worktree);
464 + if (result == -1) {
465 + /* hard error */
466 + goto force_error_stop;
467 + }
468 + if (result == -2) {
469 + /* retryable error */
470 + if (start_rdcw_watch(data, data->watch_worktree) == -1)
471 + goto force_error_stop;
472 + continue;
473 + }
474 +
475 + /* have data */
476 + if (process_worktree_events(state) == LISTENER_SHUTDOWN)
477 + goto force_shutdown;
478 + if (start_rdcw_watch(data, data->watch_worktree) == -1)
479 + goto force_error_stop;
480 + continue;
481 + }
482 +
483 + if (dwWait == WAIT_OBJECT_0 + LISTENER_HAVE_DATA_GITDIR) {
484 + result = recv_rdcw_watch(data->watch_gitdir);
485 + if (result == -1) {
486 + /* hard error */
487 + goto force_error_stop;
488 + }
489 + if (result == -2) {
490 + /* retryable error */
491 + if (start_rdcw_watch(data, data->watch_gitdir) == -1)
492 + goto force_error_stop;
493 + continue;
494 + }
495 +
496 + /* have data */
497 + if (process_gitdir_events(state) == LISTENER_SHUTDOWN)
498 + goto force_shutdown;
499 + if (start_rdcw_watch(data, data->watch_gitdir) == -1)
500 + goto force_error_stop;
501 + continue;
502 + }
503 +
504 + if (dwWait == WAIT_OBJECT_0 + LISTENER_SHUTDOWN)
505 + goto clean_shutdown;
506 +
507 + error(_("could not read directory changes [GLE %ld]"),
508 + GetLastError());
509 + goto force_error_stop;
510 + }
511 +
512 +force_error_stop:
513 + state->error_code = -1;
514 +
515 +force_shutdown:
516 + /*
517 + * Tell the IPC thead pool to stop (which completes the await
518 + * in the main thread (which will also signal this thread (if
519 + * we are still alive))).
520 + */
521 + ipc_server_stop_async(state->ipc_server_data);
522 +
523 +clean_shutdown:
524 + cancel_rdcw_watch(data->watch_worktree);
525 + cancel_rdcw_watch(data->watch_gitdir);
526 }
527
528 int fsm_listen__ctor(struct fsmonitor_daemon_state *state)
529 {
530 + struct fsmonitor_daemon_backend_data *data;
531 +
532 + CALLOC_ARRAY(data, 1);
533 +
534 + data->hEventShutdown = CreateEvent(NULL, TRUE, FALSE, NULL);
535 +
536 + data->watch_worktree = create_watch(state,
537 + state->path_worktree_watch.buf);
538 + if (!data->watch_worktree)
539 + goto failed;
540 +
541 + if (state->nr_paths_watching > 1) {
542 + data->watch_gitdir = create_watch(state,
543 + state->path_gitdir_watch.buf);
544 + if (!data->watch_gitdir)
545 + goto failed;
546 + }
547 +
548 + data->hListener[LISTENER_SHUTDOWN] = data->hEventShutdown;
549 + data->nr_listener_handles++;
550 +
551 + data->hListener[LISTENER_HAVE_DATA_WORKTREE] =
552 + data->watch_worktree->hEvent;
553 + data->nr_listener_handles++;
554 +
555 + if (data->watch_gitdir) {
556 + data->hListener[LISTENER_HAVE_DATA_GITDIR] =
557 + data->watch_gitdir->hEvent;
558 + data->nr_listener_handles++;
559 + }
560 +
561 + state->backend_data = data;
562 + return 0;
563 +
564 +failed:
565 + CloseHandle(data->hEventShutdown);
566 + destroy_watch(data->watch_worktree);
567 + destroy_watch(data->watch_gitdir);
568 +
569 return -1;
570 }
571
572 void fsm_listen__dtor(struct fsmonitor_daemon_state *state)
573 {
574 + struct fsmonitor_daemon_backend_data *data;
575 +
576 + if (!state || !state->backend_data)
577 + return;
578 +
579 + data = state->backend_data;
580 +
581 + CloseHandle(data->hEventShutdown);
582 + destroy_watch(data->watch_worktree);
583 + destroy_watch(data->watch_gitdir);
584 +
585 + FREE_AND_NULL(state->backend_data);
586 }