25
DWORD count;
26
27
struct strbuf path;
28
+ wchar_t wpath_longname[MAX_PATH + 1];
29
+ DWORD wpath_longname_len;
30
+
31
HANDLE hDir;
32
HANDLE hEvent;
33
OVERLAPPED overlapped;
37
* need to later call GetOverlappedResult() and possibly CancelIoEx().
38
*/
39
BOOL is_active;
40
+
41
+ /*
42
+ * Are shortnames enabled on the containing drive? This is
43
+ * always true for "C:/" drives and usually never true for
44
+ * other drives.
45
+ *
46
+ * We only set this for the worktree because we only need to
47
+ * convert shortname paths to longname paths for items we send
48
+ * to clients. (We don't care about shortname expansion for
49
+ * paths inside a GITDIR because we never send them to
50
+ * clients.)
51
+ */
52
+ BOOL has_shortnames;
53
+ BOOL has_tilde;
54
+ wchar_t dotgit_shortname[16]; /* for 8.3 name */
55
};
56
57
struct fsmonitor_daemon_backend_data
69
};
70
71
/*
54
- * Convert the WCHAR path from the notification into UTF8 and
55
- * then normalize it.
72
+ * Convert the WCHAR path from the event into UTF8 and normalize it.
73
+ *
74
+ * `wpath_len` is in WCHARS not bytes.
75
*/
57
-static int normalize_path_in_utf8(FILE_NOTIFY_INFORMATION *info,
76
+static int normalize_path_in_utf8(wchar_t *wpath, DWORD wpath_len,
77
struct strbuf *normalized_path)
78
{
79
int reserve;
80
int len = 0;
81
82
strbuf_reset(normalized_path);
64
- if (!info->FileNameLength)
83
+ if (!wpath_len)
84
goto normalize;
85
86
/*
89
* sequence of 2 UTF8 characters. That should let us
90
* avoid ERROR_INSUFFICIENT_BUFFER 99.9+% of the time.
91
*/
73
- reserve = info->FileNameLength + 1;
92
+ reserve = 2 * wpath_len + 1;
93
strbuf_grow(normalized_path, reserve);
94
95
for (;;) {
77
- len = WideCharToMultiByte(CP_UTF8, 0, info->FileName,
78
- info->FileNameLength / sizeof(WCHAR),
96
+ len = WideCharToMultiByte(CP_UTF8, 0,
97
+ wpath, wpath_len,
98
normalized_path->buf,
99
strbuf_avail(normalized_path) - 1,
100
NULL, NULL);
102
goto normalize;
103
if (GetLastError() != ERROR_INSUFFICIENT_BUFFER) {
104
error(_("[GLE %ld] could not convert path to UTF-8: '%.*ls'"),
86
- GetLastError(),
87
- (int)(info->FileNameLength / sizeof(WCHAR)),
88
- info->FileName);
105
+ GetLastError(), (int)wpath_len, wpath);
106
return -1;
107
}
108
115
return strbuf_normalize_path(normalized_path);
116
}
117
118
+/*
119
+ * See if the worktree root directory has shortnames enabled.
120
+ * This will help us decide if we need to do an expensive shortname
121
+ * to longname conversion on every notification event.
122
+ *
123
+ * We do not want to create a file to test this, so we assume that the
124
+ * root directory contains a ".git" file or directory. (Our caller
125
+ * only calls us for the worktree root, so this should be fine.)
126
+ *
127
+ * Remember the spelling of the shortname for ".git" if it exists.
128
+ */
129
+static void check_for_shortnames(struct one_watch *watch)
130
+{
131
+ wchar_t buf_in[MAX_PATH + 1];
132
+ wchar_t buf_out[MAX_PATH + 1];
133
+ wchar_t *last;
134
+ wchar_t *p;
135
+
136
+ /* build L"<wt-root-path>/.git" */
137
+ swprintf(buf_in, ARRAY_SIZE(buf_in) - 1, L"%ls.git",
138
+ watch->wpath_longname);
139
+
140
+ if (!GetShortPathNameW(buf_in, buf_out, ARRAY_SIZE(buf_out)))
141
+ return;
142
+
143
+ /*
144
+ * Get the final filename component of the shortpath.
145
+ * We know that the path does not have a final slash.
146
+ */
147
+ for (last = p = buf_out; *p; p++)
148
+ if (*p == L'/' || *p == '\\')
149
+ last = p + 1;
150
+
151
+ if (!wcscmp(last, L".git"))
152
+ return;
153
+
154
+ watch->has_shortnames = 1;
155
+ wcsncpy(watch->dotgit_shortname, last,
156
+ ARRAY_SIZE(watch->dotgit_shortname));
157
+
158
+ /*
159
+ * The shortname for ".git" is usually of the form "GIT~1", so
160
+ * we should be able to avoid shortname to longname mapping on
161
+ * every notification event if the source string does not
162
+ * contain a "~".
163
+ *
164
+ * However, the documentation for GetLongPathNameW() says
165
+ * that there are filesystems that don't follow that pattern
166
+ * and warns against this optimization.
167
+ *
168
+ * Lets test this.
169
+ */
170
+ if (wcschr(watch->dotgit_shortname, L'~'))
171
+ watch->has_tilde = 1;
172
+}
173
+
174
+enum get_relative_result {
175
+ GRR_NO_CONVERSION_NEEDED,
176
+ GRR_HAVE_CONVERSION,
177
+ GRR_SHUTDOWN,
178
+};
179
+
180
+/*
181
+ * Info notification paths are relative to the root of the watch.
182
+ * If our CWD is still at the root, then we can use relative paths
183
+ * to convert from shortnames to longnames. If our process has a
184
+ * different CWD, then we need to construct an absolute path, do
185
+ * the conversion, and then return the root-relative portion.
186
+ *
187
+ * We use the longname form of the root as our basis and assume that
188
+ * it already has a trailing slash.
189
+ *
190
+ * `wpath_len` is in WCHARS not bytes.
191
+ */
192
+static enum get_relative_result get_relative_longname(
193
+ struct one_watch *watch,
194
+ const wchar_t *wpath, DWORD wpath_len,
195
+ wchar_t *wpath_longname, size_t bufsize_wpath_longname)
196
+{
197
+ wchar_t buf_in[2 * MAX_PATH + 1];
198
+ wchar_t buf_out[MAX_PATH + 1];
199
+ DWORD root_len;
200
+ DWORD out_len;
201
+
202
+ /*
203
+ * Build L"<wt-root-path>/<event-rel-path>"
204
+ * Note that the <event-rel-path> might not be null terminated
205
+ * so we avoid swprintf() constructions.
206
+ */
207
+ root_len = watch->wpath_longname_len;
208
+ if (root_len + wpath_len >= ARRAY_SIZE(buf_in)) {
209
+ /*
210
+ * This should not happen. We cannot append the observed
211
+ * relative path onto the end of the worktree root path
212
+ * without overflowing the buffer. Just give up.
213
+ */
214
+ return GRR_SHUTDOWN;
215
+ }
216
+ wcsncpy(buf_in, watch->wpath_longname, root_len);
217
+ wcsncpy(buf_in + root_len, wpath, wpath_len);
218
+ buf_in[root_len + wpath_len] = 0;
219
+
220
+ /*
221
+ * We don't actually know if the source pathname is a
222
+ * shortname or a longname. This Windows routine allows
223
+ * either to be given as input.
224
+ */
225
+ out_len = GetLongPathNameW(buf_in, buf_out, ARRAY_SIZE(buf_out));
226
+ if (!out_len) {
227
+ /*
228
+ * The shortname to longname conversion can fail for
229
+ * various reasons, for example if the file has been
230
+ * deleted. (That is, if we just received a
231
+ * delete-file notification event and the file is
232
+ * already gone, we can't ask the file system to
233
+ * lookup the longname for it. Likewise, for moves
234
+ * and renames where we are given the old name.)
235
+ *
236
+ * Since deleting or moving a file or directory by its
237
+ * shortname is rather obscure, I'm going ignore the
238
+ * failure and ask the caller to report the original
239
+ * relative path. This seems kinder than failing here
240
+ * and forcing a resync. Besides, forcing a resync on
241
+ * every file/directory delete would effectively
242
+ * cripple monitoring.
243
+ *
244
+ * We might revisit this in the future.
245
+ */
246
+ return GRR_NO_CONVERSION_NEEDED;
247
+ }
248
+
249
+ if (!wcscmp(buf_in, buf_out)) {
250
+ /*
251
+ * The path does not have a shortname alias.
252
+ */
253
+ return GRR_NO_CONVERSION_NEEDED;
254
+ }
255
+
256
+ if (wcsncmp(buf_in, buf_out, root_len)) {
257
+ /*
258
+ * The spelling of the root directory portion of the computed
259
+ * longname has changed. This should not happen. Basically,
260
+ * it means that we don't know where (without recomputing the
261
+ * longname of just the root directory) to split out the
262
+ * relative path. Since this should not happen, I'm just
263
+ * going to let this fail and force a shutdown (because all
264
+ * subsequent events are probably going to see the same
265
+ * mismatch).
266
+ */
267
+ return GRR_SHUTDOWN;
268
+ }
269
+
270
+ if (out_len - root_len >= bufsize_wpath_longname) {
271
+ /*
272
+ * This should not happen. We cannot copy the root-relative
273
+ * portion of the path into the provided buffer without an
274
+ * overrun. Just give up.
275
+ */
276
+ return GRR_SHUTDOWN;
277
+ }
278
+
279
+ /* Return the worktree root-relative portion of the longname. */
280
+
281
+ wcscpy(wpath_longname, buf_out + root_len);
282
+ return GRR_HAVE_CONVERSION;
283
+}
284
+
285
void fsm_listen__stop_async(struct fsmonitor_daemon_state *state)
286
{
287
SetEvent(state->backend_data->hListener[LISTENER_SHUTDOWN]);
295
DWORD share_mode =
296
FILE_SHARE_WRITE | FILE_SHARE_READ | FILE_SHARE_DELETE;
297
HANDLE hDir;
114
- wchar_t wpath[MAX_PATH];
298
+ DWORD len_longname;
299
+ wchar_t wpath[MAX_PATH + 1];
300
+ wchar_t wpath_longname[MAX_PATH + 1];
301
302
if (xutftowcs_path(wpath, path) < 0) {
303
error(_("could not convert to wide characters: '%s'"), path);
314
return NULL;
315
}
316
317
+ len_longname = GetLongPathNameW(wpath, wpath_longname,
318
+ ARRAY_SIZE(wpath_longname));
319
+ if (!len_longname) {
320
+ error(_("[GLE %ld] could not get longname of '%s'"),
321
+ GetLastError(), path);
322
+ CloseHandle(hDir);
323
+ return NULL;
324
+ }
325
+
326
+ if (wpath_longname[len_longname - 1] != L'/' &&
327
+ wpath_longname[len_longname - 1] != L'\\') {
328
+ wpath_longname[len_longname++] = L'/';
329
+ wpath_longname[len_longname] = 0;
330
+ }
331
+
332
CALLOC_ARRAY(watch, 1);
333
334
watch->buf_len = sizeof(watch->buffer); /* assume full MAX_RDCW_BUF */
336
strbuf_init(&watch->path, 0);
337
strbuf_addstr(&watch->path, path);
338
339
+ wcscpy(watch->wpath_longname, wpath_longname);
340
+ watch->wpath_longname_len = len_longname;
341
+
342
watch->hDir = hDir;
343
watch->hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
344
462
watch->is_active = FALSE;
463
}
464
465
+/*
466
+ * Process a single relative pathname event.
467
+ * Return 1 if we should shutdown.
468
+ */
469
+static int process_1_worktree_event(
470
+ struct string_list *cookie_list,
471
+ struct fsmonitor_batch **batch,
472
+ const struct strbuf *path,
473
+ enum fsmonitor_path_type t,
474
+ DWORD info_action)
475
+{
476
+ const char *slash;
477
+
478
+ switch (t) {
479
+ case IS_INSIDE_DOT_GIT_WITH_COOKIE_PREFIX:
480
+ /* special case cookie files within .git */
481
+
482
+ /* Use just the filename of the cookie file. */
483
+ slash = find_last_dir_sep(path->buf);
484
+ string_list_append(cookie_list,
485
+ slash ? slash + 1 : path->buf);
486
+ break;
487
+
488
+ case IS_INSIDE_DOT_GIT:
489
+ /* ignore everything inside of "<worktree>/.git/" */
490
+ break;
491
+
492
+ case IS_DOT_GIT:
493
+ /* "<worktree>/.git" was deleted (or renamed away) */
494
+ if ((info_action == FILE_ACTION_REMOVED) ||
495
+ (info_action == FILE_ACTION_RENAMED_OLD_NAME)) {
496
+ trace2_data_string("fsmonitor", NULL,
497
+ "fsm-listen/dotgit",
498
+ "removed");
499
+ return 1;
500
+ }
501
+ break;
502
+
503
+ case IS_WORKDIR_PATH:
504
+ /* queue normal pathname */
505
+ if (!*batch)
506
+ *batch = fsmonitor_batch__new();
507
+ fsmonitor_batch__add_path(*batch, path->buf);
508
+ break;
509
+
510
+ case IS_GITDIR:
511
+ case IS_INSIDE_GITDIR:
512
+ case IS_INSIDE_GITDIR_WITH_COOKIE_PREFIX:
513
+ default:
514
+ BUG("unexpected path classification '%d' for '%s'",
515
+ t, path->buf);
516
+ }
517
+
518
+ return 0;
519
+}
520
+
521
/*
522
* Process filesystem events that happen anywhere (recursively) under the
523
* <worktree> root directory. For a normal working directory, this includes
534
struct string_list cookie_list = STRING_LIST_INIT_DUP;
535
struct fsmonitor_batch *batch = NULL;
536
const char *p = watch->buffer;
537
+ wchar_t wpath_longname[MAX_PATH + 1];
538
539
/*
540
* If the kernel gets more events than will fit in the kernel
567
*/
568
for (;;) {
569
FILE_NOTIFY_INFORMATION *info = (void *)p;
309
- const char *slash;
570
+ wchar_t *wpath = info->FileName;
571
+ DWORD wpath_len = info->FileNameLength / sizeof(WCHAR);
572
enum fsmonitor_path_type t;
573
+ enum get_relative_result grr;
574
+
575
+ if (watch->has_shortnames) {
576
+ if (!wcscmp(wpath, watch->dotgit_shortname)) {
577
+ /*
578
+ * This event exactly matches the
579
+ * spelling of the shortname of
580
+ * ".git", so we can skip some steps.
581
+ *
582
+ * (This case is odd because the user
583
+ * can "rm -rf GIT~1" and we cannot
584
+ * use the filesystem to map it back
585
+ * to ".git".)
586
+ */
587
+ strbuf_reset(&path);
588
+ strbuf_addstr(&path, ".git");
589
+ t = IS_DOT_GIT;
590
+ goto process_it;
591
+ }
592
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;
593
+ if (watch->has_tilde && !wcschr(wpath, L'~')) {
594
+ /*
595
+ * Shortnames on this filesystem have tildes
596
+ * and the notification path does not have
597
+ * one, so we assume that it is a longname.
598
+ */
599
+ goto normalize_it;
600
+ }
601
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");
602
+ grr = get_relative_longname(watch, wpath, wpath_len,
603
+ wpath_longname,
604
+ ARRAY_SIZE(wpath_longname));
605
+ switch (grr) {
606
+ case GRR_NO_CONVERSION_NEEDED: /* use info buffer as is */
607
+ break;
608
+ case GRR_HAVE_CONVERSION:
609
+ wpath = wpath_longname;
610
+ wpath_len = wcslen(wpath);
611
+ break;
612
+ default:
613
+ case GRR_SHUTDOWN:
614
goto force_shutdown;
615
}
341
- break;
616
+ }
617
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;
618
+normalize_it:
619
+ if (normalize_path_in_utf8(wpath, wpath_len, &path) == -1)
620
+ goto skip_this_path;
621
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
- }
622
+ t = fsmonitor_classify_path_workdir_relative(path.buf);
623
+
624
+process_it:
625
+ if (process_1_worktree_event(&cookie_list, &batch, &path, t,
626
+ info->Action))
627
+ goto force_shutdown;
628
629
skip_this_path:
630
if (!info->NextEntryOffset)
653
* Note that we DO NOT get filesystem events on the external <gitdir>
654
* itself (it is not inside something that we are watching). In particular,
655
* we do not get an event if the external <gitdir> is deleted.
656
+ *
657
+ * Also, we do not care about shortnames within the external <gitdir>, since
658
+ * we never send these paths to clients.
659
*/
660
static int process_gitdir_events(struct fsmonitor_daemon_state *state)
661
{
677
const char *slash;
678
enum fsmonitor_path_type t;
679
406
- strbuf_reset(&path);
407
- if (normalize_path_in_utf8(info, &path) == -1)
680
+ if (normalize_path_in_utf8(
681
+ info->FileName,
682
+ info->FileNameLength / sizeof(WCHAR),
683
+ &path) == -1)
684
goto skip_this_path;
685
686
t = fsmonitor_classify_path_gitdir_relative(path.buf);
814
if (!data->watch_worktree)
815
goto failed;
816
817
+ check_for_shortnames(data->watch_worktree);
818
+
819
if (state->nr_paths_watching > 1) {
820
data->watch_gitdir = create_watch(state,
821
state->path_gitdir_watch.buf);