Skip to content

Commit 6504cfd

Browse files
jeffhostetlergitster
authored andcommitted
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 <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent 90a70fa commit 6504cfd

File tree

1 file changed

+143
-0
lines changed

1 file changed

+143
-0
lines changed

compat/fsmonitor/fsm-health-win32.c

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,150 @@ struct fsm_health_data
2929
HANDLE hHandles[1]; /* the array does not own these handles */
3030
#define HEALTH_SHUTDOWN 0
3131
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;
3238
};
3339

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+
34176
int fsm_health__ctor(struct fsmonitor_daemon_state *state)
35177
{
36178
struct fsm_health_data *data;
@@ -64,6 +206,7 @@ void fsm_health__dtor(struct fsmonitor_daemon_state *state)
64206
* A table of the polling functions.
65207
*/
66208
static interval_fn *table[] = {
209+
has_worktree_moved,
67210
NULL, /* must be last */
68211
};
69212

0 commit comments

Comments
 (0)