#include "git-compat-util.h" #include "config.h" #include "fsmonitor-ll.h" #include "fsm-health.h" #include "fsmonitor--daemon.h" #include "gettext.h" #include "simple-ipc.h" /* * Every minute wake up and test our health. */ #define WAIT_FREQ_MS (60 * 1000) /* * State machine states for each of the interval functions * used for polling our health. */ enum interval_fn_ctx { CTX_INIT = 0, CTX_TERM, CTX_TIMER }; typedef int (interval_fn)(struct fsmonitor_daemon_state *state, enum interval_fn_ctx ctx); struct fsm_health_data { HANDLE hEventShutdown; 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_LONG_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_long_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; CALLOC_ARRAY(data, 1); data->hEventShutdown = CreateEvent(NULL, TRUE, FALSE, NULL); data->hHandles[HEALTH_SHUTDOWN] = data->hEventShutdown; data->nr_handles++; state->health_data = data; return 0; } void fsm_health__dtor(struct fsmonitor_daemon_state *state) { struct fsm_health_data *data; if (!state || !state->health_data) return; data = state->health_data; CloseHandle(data->hEventShutdown); FREE_AND_NULL(state->health_data); } /* * A table of the polling functions. */ static interval_fn *table[] = { has_worktree_moved, NULL, /* must be last */ }; /* * Call all of the polling functions in the table. * Shortcut and return first error. * * Return 0 if all succeeded. */ static int call_all(struct fsmonitor_daemon_state *state, enum interval_fn_ctx ctx) { int k; for (k = 0; table[k]; k++) { int r = table[k](state, ctx); if (r) return r; } return 0; } void fsm_health__loop(struct fsmonitor_daemon_state *state) { struct fsm_health_data *data = state->health_data; int r; r = call_all(state, CTX_INIT); if (r < 0) goto force_error_stop; if (r > 0) goto force_shutdown; for (;;) { DWORD dwWait = WaitForMultipleObjects(data->nr_handles, data->hHandles, FALSE, WAIT_FREQ_MS); if (dwWait == WAIT_OBJECT_0 + HEALTH_SHUTDOWN) goto clean_shutdown; if (dwWait == WAIT_TIMEOUT) { r = call_all(state, CTX_TIMER); if (r < 0) goto force_error_stop; if (r > 0) goto force_shutdown; continue; } error(_("health thread wait failed [GLE %ld]"), GetLastError()); goto force_error_stop; } force_error_stop: state->health_error_code = -1; force_shutdown: ipc_server_stop_async(state->ipc_server_data); clean_shutdown: call_all(state, CTX_TERM); return; } void fsm_health__stop_async(struct fsmonitor_daemon_state *state) { SetEvent(state->health_data->hHandles[HEALTH_SHUTDOWN]); }