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
}