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 committedMay 26, 2022 at 21:47 UTC6504cfd392936f6e3994216101659fcda24ed1ac
1 file changed+143
compat/fsmonitor/fsm-health-win32.c
+143
index 24fc612bf0..2ea08c1d4e 100644--- a/compat/fsmonitor/fsm-health-win32.c+++ b/compat/fsmonitor/fsm-health-win32.c@@ -29,8 +29,150 @@ struct fsm_health_data HANDLE hHandles[1]; /* the array does not own these handles */ #define HEALTH_SHUTDOWN 0 int nr_handles; /* number of active event handles */++ struct wt_moved+ {+ wchar_t wpath[MAX_PATH + 1];+ BY_HANDLE_FILE_INFORMATION bhfi;+ } wt_moved; };+/*+ * Lookup the system unique ID for the path. This is as close as+ * we get to an inode number, but this also contains volume info,+ * so it is a little stronger.+ */+static int lookup_bhfi(wchar_t *wpath,+ BY_HANDLE_FILE_INFORMATION *bhfi)+{+ DWORD desired_access = FILE_LIST_DIRECTORY;+ DWORD share_mode =+ FILE_SHARE_WRITE | FILE_SHARE_READ | FILE_SHARE_DELETE;+ HANDLE hDir;++ hDir = CreateFileW(wpath, desired_access, share_mode, NULL,+ OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL);+ if (hDir == INVALID_HANDLE_VALUE) {+ error(_("[GLE %ld] health thread could not open '%ls'"),+ GetLastError(), wpath);+ return -1;+ }++ if (!GetFileInformationByHandle(hDir, bhfi)) {+ error(_("[GLE %ld] health thread getting BHFI for '%ls'"),+ GetLastError(), wpath);+ CloseHandle(hDir);+ return -1;+ }++ CloseHandle(hDir);+ return 0;+}++/*+ * Compare the relevant fields from two system unique IDs.+ * We use this to see if two different handles to the same+ * path actually refer to the same *instance* of the file+ * or directory.+ */+static int bhfi_eq(const BY_HANDLE_FILE_INFORMATION *bhfi_1,+ const BY_HANDLE_FILE_INFORMATION *bhfi_2)+{+ return (bhfi_1->dwVolumeSerialNumber == bhfi_2->dwVolumeSerialNumber &&+ bhfi_1->nFileIndexHigh == bhfi_2->nFileIndexHigh &&+ bhfi_1->nFileIndexLow == bhfi_2->nFileIndexLow);+}++/*+ * Shutdown if the original worktree root directory been deleted,+ * moved, or renamed?+ *+ * Since the main thread did a "chdir(getenv($HOME))" and our CWD+ * is not in the worktree root directory and because the listener+ * thread added FILE_SHARE_DELETE to the watch handle, it is possible+ * for the root directory to be moved or deleted while we are still+ * watching it. We want to detect that here and force a shutdown.+ *+ * Granted, a delete MAY cause some operations to fail, such as+ * GetOverlappedResult(), but it is not guaranteed. And because+ * ReadDirectoryChangesW() only reports on changes *WITHIN* the+ * directory, not changes *ON* the directory, our watch will not+ * receive a delete event for it.+ *+ * A move/rename of the worktree root will also not generate an event.+ * And since the listener thread already has an open handle, it may+ * continue to receive events for events within the directory.+ * However, the pathname of the named-pipe was constructed using the+ * original location of the worktree root. (Remember named-pipes are+ * stored in the NPFS and not in the actual file system.) Clients+ * trying to talk to the worktree after the move/rename will not+ * reach our daemon process, since we're still listening on the+ * pipe with original path.+ *+ * Furthermore, if the user does something like:+ *+ * $ mv repo repo.old+ * $ git init repo+ *+ * A new daemon cannot be started in the new instance of "repo"+ * because the named-pipe is still being used by the daemon on+ * the original instance.+ *+ * So, detect move/rename/delete and shutdown. This should also+ * handle unsafe drive removal.+ *+ * We use the file system unique ID to distinguish the original+ * directory instance from a new instance and force a shutdown+ * if the unique ID changes.+ *+ * Since a worktree move/rename/delete/unmount doesn't happen+ * that often (and we can't get an immediate event anyway), we+ * use a timeout and periodically poll it.+ */+static int has_worktree_moved(struct fsmonitor_daemon_state *state,+ enum interval_fn_ctx ctx)+{+ struct fsm_health_data *data = state->health_data;+ BY_HANDLE_FILE_INFORMATION bhfi;+ int r;++ switch (ctx) {+ case CTX_TERM:+ return 0;++ case CTX_INIT:+ if (xutftowcs_path(data->wt_moved.wpath,+ state->path_worktree_watch.buf) < 0) {+ error(_("could not convert to wide characters: '%s'"),+ state->path_worktree_watch.buf);+ return -1;+ }++ /*+ * On the first call we lookup the unique sequence ID for+ * the worktree root directory.+ */+ return lookup_bhfi(data->wt_moved.wpath, &data->wt_moved.bhfi);++ case CTX_TIMER:+ r = lookup_bhfi(data->wt_moved.wpath, &bhfi);+ if (r)+ return r;+ if (!bhfi_eq(&data->wt_moved.bhfi, &bhfi)) {+ error(_("BHFI changed '%ls'"), data->wt_moved.wpath);+ return -1;+ }+ return 0;++ default:+ die(_("unhandled case in 'has_worktree_moved': %d"),+ (int)ctx);+ }++ return 0;+}++ int fsm_health__ctor(struct fsmonitor_daemon_state *state) { struct fsm_health_data *data;@@ -64,6 +206,7 @@ void fsm_health__dtor(struct fsmonitor_daemon_state *state) * A table of the polling functions. */ static interval_fn *table[] = {+ has_worktree_moved, NULL, /* must be last */ };