fsm-health-win32: force shutdown daemon if worktree root moves

Force shutdown fsmonitor daemon if the worktree root directory is moved, renamed, or deleted. Use Windows low-level GetFileInformationByHandle() to get and compare the Windows system unique ID for the directory with a cached version when we started up. This lets us detect the case where someone renames the directory that we are watching and then creates a new directory with the original pathname. This is important because we are listening to a named pipe for requests and they are stored in the Named Pipe File System (NPFS) which a kernel-resident pseudo filesystem not associated with the actual NTFS directory. For example, if the daemon was watching "~/foo/", it would have a directory-watch handle on that directory and a named-pipe handle for "//./pipe/...foo". Moving the directory to "~/bar/" does not invalidate the directory handle. (So the daemon would actually be watching "~/bar" but listening on "//./pipe/...foo". If the user then does "git init ~/foo" and causes another daemon to start, the first daemon will still have ownership of the pipe and the second daemon instance will fail to start. "git status" clients in "~/foo" will ask "//./pipe/...foo" about changes and the first daemon instance will tell them about "~/bar". This commit causes the first daemon to shutdown if the system unique ID for "~/foo" changes (changes from what it was when the daemon started). Shutdown occurs after a periodic poll. After the first daemon exits and releases the lock on the named pipe, subsequent Git commands may cause another daemon to be started on "~/foo". Similarly, a subsequent Git command may cause another daemon to be started on "~/bar". Signed-off-by: Jeff Hostetler <jeffhost@microsoft.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Jeff Hostetler committed May 26, 2022 at 21:47 UTC 6504cfd392936f6e3994216101659fcda24ed1ac
1 file changed +143
compat/fsmonitor/fsm-health-win32.c
+143
@@ -29,8 +29,150 @@ struct fsm_health_data
29 HANDLE hHandles[1]; /* the array does not own these handles */
30 #define HEALTH_SHUTDOWN 0
31 int nr_handles; /* number of active event handles */
32 +
33 + struct wt_moved
34 + {
35 + wchar_t wpath[MAX_PATH + 1];
36 + BY_HANDLE_FILE_INFORMATION bhfi;
37 + } wt_moved;
38 };
39
40 +/*
41 + * Lookup the system unique ID for the path. This is as close as
42 + * we get to an inode number, but this also contains volume info,
43 + * so it is a little stronger.
44 + */
45 +static int lookup_bhfi(wchar_t *wpath,
46 + BY_HANDLE_FILE_INFORMATION *bhfi)
47 +{
48 + DWORD desired_access = FILE_LIST_DIRECTORY;
49 + DWORD share_mode =
50 + FILE_SHARE_WRITE | FILE_SHARE_READ | FILE_SHARE_DELETE;
51 + HANDLE hDir;
52 +
53 + hDir = CreateFileW(wpath, desired_access, share_mode, NULL,
54 + OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL);
55 + if (hDir == INVALID_HANDLE_VALUE) {
56 + error(_("[GLE %ld] health thread could not open '%ls'"),
57 + GetLastError(), wpath);
58 + return -1;
59 + }
60 +
61 + if (!GetFileInformationByHandle(hDir, bhfi)) {
62 + error(_("[GLE %ld] health thread getting BHFI for '%ls'"),
63 + GetLastError(), wpath);
64 + CloseHandle(hDir);
65 + return -1;
66 + }
67 +
68 + CloseHandle(hDir);
69 + return 0;
70 +}
71 +
72 +/*
73 + * Compare the relevant fields from two system unique IDs.
74 + * We use this to see if two different handles to the same
75 + * path actually refer to the same *instance* of the file
76 + * or directory.
77 + */
78 +static int bhfi_eq(const BY_HANDLE_FILE_INFORMATION *bhfi_1,
79 + const BY_HANDLE_FILE_INFORMATION *bhfi_2)
80 +{
81 + return (bhfi_1->dwVolumeSerialNumber == bhfi_2->dwVolumeSerialNumber &&
82 + bhfi_1->nFileIndexHigh == bhfi_2->nFileIndexHigh &&
83 + bhfi_1->nFileIndexLow == bhfi_2->nFileIndexLow);
84 +}
85 +
86 +/*
87 + * Shutdown if the original worktree root directory been deleted,
88 + * moved, or renamed?
89 + *
90 + * Since the main thread did a "chdir(getenv($HOME))" and our CWD
91 + * is not in the worktree root directory and because the listener
92 + * thread added FILE_SHARE_DELETE to the watch handle, it is possible
93 + * for the root directory to be moved or deleted while we are still
94 + * watching it. We want to detect that here and force a shutdown.
95 + *
96 + * Granted, a delete MAY cause some operations to fail, such as
97 + * GetOverlappedResult(), but it is not guaranteed. And because
98 + * ReadDirectoryChangesW() only reports on changes *WITHIN* the
99 + * directory, not changes *ON* the directory, our watch will not
100 + * receive a delete event for it.
101 + *
102 + * A move/rename of the worktree root will also not generate an event.
103 + * And since the listener thread already has an open handle, it may
104 + * continue to receive events for events within the directory.
105 + * However, the pathname of the named-pipe was constructed using the
106 + * original location of the worktree root. (Remember named-pipes are
107 + * stored in the NPFS and not in the actual file system.) Clients
108 + * trying to talk to the worktree after the move/rename will not
109 + * reach our daemon process, since we're still listening on the
110 + * pipe with original path.
111 + *
112 + * Furthermore, if the user does something like:
113 + *
114 + * $ mv repo repo.old
115 + * $ git init repo
116 + *
117 + * A new daemon cannot be started in the new instance of "repo"
118 + * because the named-pipe is still being used by the daemon on
119 + * the original instance.
120 + *
121 + * So, detect move/rename/delete and shutdown. This should also
122 + * handle unsafe drive removal.
123 + *
124 + * We use the file system unique ID to distinguish the original
125 + * directory instance from a new instance and force a shutdown
126 + * if the unique ID changes.
127 + *
128 + * Since a worktree move/rename/delete/unmount doesn't happen
129 + * that often (and we can't get an immediate event anyway), we
130 + * use a timeout and periodically poll it.
131 + */
132 +static int has_worktree_moved(struct fsmonitor_daemon_state *state,
133 + enum interval_fn_ctx ctx)
134 +{
135 + struct fsm_health_data *data = state->health_data;
136 + BY_HANDLE_FILE_INFORMATION bhfi;
137 + int r;
138 +
139 + switch (ctx) {
140 + case CTX_TERM:
141 + return 0;
142 +
143 + case CTX_INIT:
144 + if (xutftowcs_path(data->wt_moved.wpath,
145 + state->path_worktree_watch.buf) < 0) {
146 + error(_("could not convert to wide characters: '%s'"),
147 + state->path_worktree_watch.buf);
148 + return -1;
149 + }
150 +
151 + /*
152 + * On the first call we lookup the unique sequence ID for
153 + * the worktree root directory.
154 + */
155 + return lookup_bhfi(data->wt_moved.wpath, &data->wt_moved.bhfi);
156 +
157 + case CTX_TIMER:
158 + r = lookup_bhfi(data->wt_moved.wpath, &bhfi);
159 + if (r)
160 + return r;
161 + if (!bhfi_eq(&data->wt_moved.bhfi, &bhfi)) {
162 + error(_("BHFI changed '%ls'"), data->wt_moved.wpath);
163 + return -1;
164 + }
165 + return 0;
166 +
167 + default:
168 + die(_("unhandled case in 'has_worktree_moved': %d"),
169 + (int)ctx);
170 + }
171 +
172 + return 0;
173 +}
174 +
175 +
176 int fsm_health__ctor(struct fsmonitor_daemon_state *state)
177 {
178 struct fsm_health_data *data;
@@ -64,6 +206,7 @@ void fsm_health__dtor(struct fsmonitor_daemon_state *state)
206 * A table of the polling functions.
207 */
208 static interval_fn *table[] = {
209 + has_worktree_moved,
210 NULL, /* must be last */
211 };
212