]> git.ipfire.org Git - thirdparty/git.git/commitdiff
fsm-health-win32: force shutdown daemon if worktree root moves
authorJeff Hostetler <jeffhost@microsoft.com>
Thu, 26 May 2022 21:47:12 +0000 (21:47 +0000)
committerJunio C Hamano <gitster@pobox.com>
Thu, 26 May 2022 22:59:27 +0000 (15:59 -0700)
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>
compat/fsmonitor/fsm-health-win32.c

index 24fc612bf02653b68e72e98225f4feb02e5c0b1f..2ea08c1d4e81ecec5c6cada8f0e9fff76303fcc4 100644 (file)
@@ -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 */
 };