#include "cache.h" #include "config.h" #include "fsmonitor.h" #include "fsm-listen.h" #include "fsmonitor--daemon.h" /* * The documentation of ReadDirectoryChangesW() states that the maximum * buffer size is 64K when the monitored directory is remote. * * Larger buffers may be used when the monitored directory is local and * will help us receive events faster from the kernel and avoid dropped * events. * * So we try to use a very large buffer and silently fallback to 64K if * we get an error. */ #define MAX_RDCW_BUF_FALLBACK (65536) #define MAX_RDCW_BUF (65536 * 8) struct one_watch { char buffer[MAX_RDCW_BUF]; DWORD buf_len; DWORD count; struct strbuf path; wchar_t wpath_longname[MAX_PATH + 1]; DWORD wpath_longname_len; HANDLE hDir; HANDLE hEvent; OVERLAPPED overlapped; /* * Is there an active ReadDirectoryChangesW() call pending. If so, we * need to later call GetOverlappedResult() and possibly CancelIoEx(). */ BOOL is_active; /* * Are shortnames enabled on the containing drive? This is * always true for "C:/" drives and usually never true for * other drives. * * We only set this for the worktree because we only need to * convert shortname paths to longname paths for items we send * to clients. (We don't care about shortname expansion for * paths inside a GITDIR because we never send them to * clients.) */ BOOL has_shortnames; BOOL has_tilde; wchar_t dotgit_shortname[16]; /* for 8.3 name */ }; struct fsm_listen_data { struct one_watch *watch_worktree; struct one_watch *watch_gitdir; HANDLE hEventShutdown; HANDLE hListener[3]; /* we don't own these handles */ #define LISTENER_SHUTDOWN 0 #define LISTENER_HAVE_DATA_WORKTREE 1 #define LISTENER_HAVE_DATA_GITDIR 2 int nr_listener_handles; }; /* * Convert the WCHAR path from the event into UTF8 and normalize it. * * `wpath_len` is in WCHARS not bytes. */ static int normalize_path_in_utf8(wchar_t *wpath, DWORD wpath_len, struct strbuf *normalized_path) { int reserve; int len = 0; strbuf_reset(normalized_path); if (!wpath_len) goto normalize; /* * Pre-reserve enough space in the UTF8 buffer for * each Unicode WCHAR character to be mapped into a * sequence of 2 UTF8 characters. That should let us * avoid ERROR_INSUFFICIENT_BUFFER 99.9+% of the time. */ reserve = 2 * wpath_len + 1; strbuf_grow(normalized_path, reserve); for (;;) { len = WideCharToMultiByte(CP_UTF8, 0, wpath, wpath_len, normalized_path->buf, strbuf_avail(normalized_path) - 1, NULL, NULL); if (len > 0) goto normalize; if (GetLastError() != ERROR_INSUFFICIENT_BUFFER) { error(_("[GLE %ld] could not convert path to UTF-8: '%.*ls'"), GetLastError(), (int)wpath_len, wpath); return -1; } strbuf_grow(normalized_path, strbuf_avail(normalized_path) + reserve); } normalize: strbuf_setlen(normalized_path, len); return strbuf_normalize_path(normalized_path); } /* * See if the worktree root directory has shortnames enabled. * This will help us decide if we need to do an expensive shortname * to longname conversion on every notification event. * * We do not want to create a file to test this, so we assume that the * root directory contains a ".git" file or directory. (Our caller * only calls us for the worktree root, so this should be fine.) * * Remember the spelling of the shortname for ".git" if it exists. */ static void check_for_shortnames(struct one_watch *watch) { wchar_t buf_in[MAX_PATH + 1]; wchar_t buf_out[MAX_PATH + 1]; wchar_t *last; wchar_t *p; /* build L"/.git" */ swprintf(buf_in, ARRAY_SIZE(buf_in) - 1, L"%ls.git", watch->wpath_longname); if (!GetShortPathNameW(buf_in, buf_out, ARRAY_SIZE(buf_out))) return; /* * Get the final filename component of the shortpath. * We know that the path does not have a final slash. */ for (last = p = buf_out; *p; p++) if (*p == L'/' || *p == '\\') last = p + 1; if (!wcscmp(last, L".git")) return; watch->has_shortnames = 1; wcsncpy(watch->dotgit_shortname, last, ARRAY_SIZE(watch->dotgit_shortname)); /* * The shortname for ".git" is usually of the form "GIT~1", so * we should be able to avoid shortname to longname mapping on * every notification event if the source string does not * contain a "~". * * However, the documentation for GetLongPathNameW() says * that there are filesystems that don't follow that pattern * and warns against this optimization. * * Lets test this. */ if (wcschr(watch->dotgit_shortname, L'~')) watch->has_tilde = 1; } enum get_relative_result { GRR_NO_CONVERSION_NEEDED, GRR_HAVE_CONVERSION, GRR_SHUTDOWN, }; /* * Info notification paths are relative to the root of the watch. * If our CWD is still at the root, then we can use relative paths * to convert from shortnames to longnames. If our process has a * different CWD, then we need to construct an absolute path, do * the conversion, and then return the root-relative portion. * * We use the longname form of the root as our basis and assume that * it already has a trailing slash. * * `wpath_len` is in WCHARS not bytes. */ static enum get_relative_result get_relative_longname( struct one_watch *watch, const wchar_t *wpath, DWORD wpath_len, wchar_t *wpath_longname, size_t bufsize_wpath_longname) { wchar_t buf_in[2 * MAX_PATH + 1]; wchar_t buf_out[MAX_PATH + 1]; DWORD root_len; DWORD out_len; /* * Build L"/" * Note that the might not be null terminated * so we avoid swprintf() constructions. */ root_len = watch->wpath_longname_len; if (root_len + wpath_len >= ARRAY_SIZE(buf_in)) { /* * This should not happen. We cannot append the observed * relative path onto the end of the worktree root path * without overflowing the buffer. Just give up. */ return GRR_SHUTDOWN; } wcsncpy(buf_in, watch->wpath_longname, root_len); wcsncpy(buf_in + root_len, wpath, wpath_len); buf_in[root_len + wpath_len] = 0; /* * We don't actually know if the source pathname is a * shortname or a longname. This Windows routine allows * either to be given as input. */ out_len = GetLongPathNameW(buf_in, buf_out, ARRAY_SIZE(buf_out)); if (!out_len) { /* * The shortname to longname conversion can fail for * various reasons, for example if the file has been * deleted. (That is, if we just received a * delete-file notification event and the file is * already gone, we can't ask the file system to * lookup the longname for it. Likewise, for moves * and renames where we are given the old name.) * * Since deleting or moving a file or directory by its * shortname is rather obscure, I'm going ignore the * failure and ask the caller to report the original * relative path. This seems kinder than failing here * and forcing a resync. Besides, forcing a resync on * every file/directory delete would effectively * cripple monitoring. * * We might revisit this in the future. */ return GRR_NO_CONVERSION_NEEDED; } if (!wcscmp(buf_in, buf_out)) { /* * The path does not have a shortname alias. */ return GRR_NO_CONVERSION_NEEDED; } if (wcsncmp(buf_in, buf_out, root_len)) { /* * The spelling of the root directory portion of the computed * longname has changed. This should not happen. Basically, * it means that we don't know where (without recomputing the * longname of just the root directory) to split out the * relative path. Since this should not happen, I'm just * going to let this fail and force a shutdown (because all * subsequent events are probably going to see the same * mismatch). */ return GRR_SHUTDOWN; } if (out_len - root_len >= bufsize_wpath_longname) { /* * This should not happen. We cannot copy the root-relative * portion of the path into the provided buffer without an * overrun. Just give up. */ return GRR_SHUTDOWN; } /* Return the worktree root-relative portion of the longname. */ wcscpy(wpath_longname, buf_out + root_len); return GRR_HAVE_CONVERSION; } void fsm_listen__stop_async(struct fsmonitor_daemon_state *state) { SetEvent(state->listen_data->hListener[LISTENER_SHUTDOWN]); } static struct one_watch *create_watch(struct fsmonitor_daemon_state *state, const char *path) { struct one_watch *watch = NULL; DWORD desired_access = FILE_LIST_DIRECTORY; DWORD share_mode = FILE_SHARE_WRITE | FILE_SHARE_READ | FILE_SHARE_DELETE; HANDLE hDir; DWORD len_longname; wchar_t wpath[MAX_PATH + 1]; wchar_t wpath_longname[MAX_PATH + 1]; if (xutftowcs_path(wpath, path) < 0) { error(_("could not convert to wide characters: '%s'"), path); return NULL; } hDir = CreateFileW(wpath, desired_access, share_mode, NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED, NULL); if (hDir == INVALID_HANDLE_VALUE) { error(_("[GLE %ld] could not watch '%s'"), GetLastError(), path); return NULL; } len_longname = GetLongPathNameW(wpath, wpath_longname, ARRAY_SIZE(wpath_longname)); if (!len_longname) { error(_("[GLE %ld] could not get longname of '%s'"), GetLastError(), path); CloseHandle(hDir); return NULL; } if (wpath_longname[len_longname - 1] != L'/' && wpath_longname[len_longname - 1] != L'\\') { wpath_longname[len_longname++] = L'/'; wpath_longname[len_longname] = 0; } CALLOC_ARRAY(watch, 1); watch->buf_len = sizeof(watch->buffer); /* assume full MAX_RDCW_BUF */ strbuf_init(&watch->path, 0); strbuf_addstr(&watch->path, path); wcscpy(watch->wpath_longname, wpath_longname); watch->wpath_longname_len = len_longname; watch->hDir = hDir; watch->hEvent = CreateEvent(NULL, TRUE, FALSE, NULL); return watch; } static void destroy_watch(struct one_watch *watch) { if (!watch) return; strbuf_release(&watch->path); if (watch->hDir != INVALID_HANDLE_VALUE) CloseHandle(watch->hDir); if (watch->hEvent != INVALID_HANDLE_VALUE) CloseHandle(watch->hEvent); free(watch); } static int start_rdcw_watch(struct fsm_listen_data *data, struct one_watch *watch) { DWORD dwNotifyFilter = FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME | FILE_NOTIFY_CHANGE_ATTRIBUTES | FILE_NOTIFY_CHANGE_SIZE | FILE_NOTIFY_CHANGE_LAST_WRITE | FILE_NOTIFY_CHANGE_CREATION; ResetEvent(watch->hEvent); memset(&watch->overlapped, 0, sizeof(watch->overlapped)); watch->overlapped.hEvent = watch->hEvent; /* * Queue an async call using Overlapped IO. This returns immediately. * Our event handle will be signalled when the real result is available. * * The return value here just means that we successfully queued it. * We won't know if the Read...() actually produces data until later. */ watch->is_active = ReadDirectoryChangesW( watch->hDir, watch->buffer, watch->buf_len, TRUE, dwNotifyFilter, &watch->count, &watch->overlapped, NULL); if (watch->is_active) return 0; error(_("ReadDirectoryChangedW failed on '%s' [GLE %ld]"), watch->path.buf, GetLastError()); return -1; } static int recv_rdcw_watch(struct one_watch *watch) { DWORD gle; watch->is_active = FALSE; /* * The overlapped result is ready. If the Read...() was successful * we finally receive the actual result into our buffer. */ if (GetOverlappedResult(watch->hDir, &watch->overlapped, &watch->count, TRUE)) return 0; gle = GetLastError(); if (gle == ERROR_INVALID_PARAMETER && /* * The kernel throws an invalid parameter error when our * buffer is too big and we are pointed at a remote * directory (and possibly for other reasons). Quietly * set it down and try again. * * See note about MAX_RDCW_BUF at the top. */ watch->buf_len > MAX_RDCW_BUF_FALLBACK) { watch->buf_len = MAX_RDCW_BUF_FALLBACK; return -2; } /* * GetOverlappedResult() fails if the watched directory is * deleted while we were waiting for an overlapped IO to * complete. The documentation did not list specific errors, * but I observed ERROR_ACCESS_DENIED (0x05) errors during * testing. * * Note that we only get notificaiton events for events * *within* the directory, not *on* the directory itself. * (These might be properies of the parent directory, for * example). * * NEEDSWORK: We might try to check for the deleted directory * case and return a better error message, but I'm not sure it * is worth it. * * Shutdown if we get any error. */ error(_("GetOverlappedResult failed on '%s' [GLE %ld]"), watch->path.buf, gle); return -1; } static void cancel_rdcw_watch(struct one_watch *watch) { DWORD count; if (!watch || !watch->is_active) return; /* * The calls to ReadDirectoryChangesW() and GetOverlappedResult() * form a "pair" (my term) where we queue an IO and promise to * hang around and wait for the kernel to give us the result. * * If for some reason after we queue the IO, we have to quit * or otherwise not stick around for the second half, we must * tell the kernel to abort the IO. This prevents the kernel * from writing to our buffer and/or signalling our event * after we free them. * * (Ask me how much fun it was to track that one down). */ CancelIoEx(watch->hDir, &watch->overlapped); GetOverlappedResult(watch->hDir, &watch->overlapped, &count, TRUE); watch->is_active = FALSE; } /* * Process a single relative pathname event. * Return 1 if we should shutdown. */ static int process_1_worktree_event( struct string_list *cookie_list, struct fsmonitor_batch **batch, const struct strbuf *path, enum fsmonitor_path_type t, DWORD info_action) { const char *slash; switch (t) { case IS_INSIDE_DOT_GIT_WITH_COOKIE_PREFIX: /* special case cookie files within .git */ /* Use just the filename of the cookie file. */ slash = find_last_dir_sep(path->buf); string_list_append(cookie_list, slash ? slash + 1 : path->buf); break; case IS_INSIDE_DOT_GIT: /* ignore everything inside of "/.git/" */ break; case IS_DOT_GIT: /* "/.git" was deleted (or renamed away) */ if ((info_action == FILE_ACTION_REMOVED) || (info_action == FILE_ACTION_RENAMED_OLD_NAME)) { trace2_data_string("fsmonitor", NULL, "fsm-listen/dotgit", "removed"); return 1; } break; case IS_WORKDIR_PATH: /* queue normal pathname */ if (!*batch) *batch = fsmonitor_batch__new(); fsmonitor_batch__add_path(*batch, path->buf); break; case IS_GITDIR: case IS_INSIDE_GITDIR: case IS_INSIDE_GITDIR_WITH_COOKIE_PREFIX: default: BUG("unexpected path classification '%d' for '%s'", t, path->buf); } return 0; } /* * Process filesystem events that happen anywhere (recursively) under the * root directory. For a normal working directory, this includes * both version controlled files and the contents of the .git/ directory. * * If /.git is a file, then we only see events for the file * itself. */ static int process_worktree_events(struct fsmonitor_daemon_state *state) { struct fsm_listen_data *data = state->listen_data; struct one_watch *watch = data->watch_worktree; struct strbuf path = STRBUF_INIT; struct string_list cookie_list = STRING_LIST_INIT_DUP; struct fsmonitor_batch *batch = NULL; const char *p = watch->buffer; wchar_t wpath_longname[MAX_PATH + 1]; /* * If the kernel gets more events than will fit in the kernel * buffer associated with our RDCW handle, it drops them and * returns a count of zero. * * Yes, the call returns WITHOUT error and with length zero. * This is the documented behavior. (My testing has confirmed * that it also sets the last error to ERROR_NOTIFY_ENUM_DIR, * but we do not rely on that since the function did not * return an error and it is not documented.) * * (The "overflow" case is not ambiguous with the "no data" case * because we did an INFINITE wait.) * * This means we have a gap in coverage. Tell the daemon layer * to resync. */ if (!watch->count) { trace2_data_string("fsmonitor", NULL, "fsm-listen/kernel", "overflow"); fsmonitor_force_resync(state); return LISTENER_HAVE_DATA_WORKTREE; } /* * On Windows, `info` contains an "array" of paths that are * relative to the root of whichever directory handle received * the event. */ for (;;) { FILE_NOTIFY_INFORMATION *info = (void *)p; wchar_t *wpath = info->FileName; DWORD wpath_len = info->FileNameLength / sizeof(WCHAR); enum fsmonitor_path_type t; enum get_relative_result grr; if (watch->has_shortnames) { if (!wcscmp(wpath, watch->dotgit_shortname)) { /* * This event exactly matches the * spelling of the shortname of * ".git", so we can skip some steps. * * (This case is odd because the user * can "rm -rf GIT~1" and we cannot * use the filesystem to map it back * to ".git".) */ strbuf_reset(&path); strbuf_addstr(&path, ".git"); t = IS_DOT_GIT; goto process_it; } if (watch->has_tilde && !wcschr(wpath, L'~')) { /* * Shortnames on this filesystem have tildes * and the notification path does not have * one, so we assume that it is a longname. */ goto normalize_it; } grr = get_relative_longname(watch, wpath, wpath_len, wpath_longname, ARRAY_SIZE(wpath_longname)); switch (grr) { case GRR_NO_CONVERSION_NEEDED: /* use info buffer as is */ break; case GRR_HAVE_CONVERSION: wpath = wpath_longname; wpath_len = wcslen(wpath); break; default: case GRR_SHUTDOWN: goto force_shutdown; } } normalize_it: if (normalize_path_in_utf8(wpath, wpath_len, &path) == -1) goto skip_this_path; t = fsmonitor_classify_path_workdir_relative(path.buf); process_it: if (process_1_worktree_event(&cookie_list, &batch, &path, t, info->Action)) goto force_shutdown; skip_this_path: if (!info->NextEntryOffset) break; p += info->NextEntryOffset; } fsmonitor_publish(state, batch, &cookie_list); batch = NULL; string_list_clear(&cookie_list, 0); strbuf_release(&path); return LISTENER_HAVE_DATA_WORKTREE; force_shutdown: fsmonitor_batch__free_list(batch); string_list_clear(&cookie_list, 0); strbuf_release(&path); return LISTENER_SHUTDOWN; } /* * Process filesystem events that happened anywhere (recursively) under the * external (such as non-primary worktrees or submodules). * We only care about cookie files that our client threads created here. * * Note that we DO NOT get filesystem events on the external * itself (it is not inside something that we are watching). In particular, * we do not get an event if the external is deleted. * * Also, we do not care about shortnames within the external , since * we never send these paths to clients. */ static int process_gitdir_events(struct fsmonitor_daemon_state *state) { struct fsm_listen_data *data = state->listen_data; struct one_watch *watch = data->watch_gitdir; struct strbuf path = STRBUF_INIT; struct string_list cookie_list = STRING_LIST_INIT_DUP; const char *p = watch->buffer; if (!watch->count) { trace2_data_string("fsmonitor", NULL, "fsm-listen/kernel", "overflow"); fsmonitor_force_resync(state); return LISTENER_HAVE_DATA_GITDIR; } for (;;) { FILE_NOTIFY_INFORMATION *info = (void *)p; const char *slash; enum fsmonitor_path_type t; if (normalize_path_in_utf8( info->FileName, info->FileNameLength / sizeof(WCHAR), &path) == -1) goto skip_this_path; t = fsmonitor_classify_path_gitdir_relative(path.buf); switch (t) { case IS_INSIDE_GITDIR_WITH_COOKIE_PREFIX: /* special case cookie files within gitdir */ /* Use just the filename of the cookie file. */ slash = find_last_dir_sep(path.buf); string_list_append(&cookie_list, slash ? slash + 1 : path.buf); break; case IS_INSIDE_GITDIR: goto skip_this_path; default: BUG("unexpected path classification '%d' for '%s'", t, path.buf); } skip_this_path: if (!info->NextEntryOffset) break; p += info->NextEntryOffset; } fsmonitor_publish(state, NULL, &cookie_list); string_list_clear(&cookie_list, 0); strbuf_release(&path); return LISTENER_HAVE_DATA_GITDIR; } void fsm_listen__loop(struct fsmonitor_daemon_state *state) { struct fsm_listen_data *data = state->listen_data; DWORD dwWait; int result; state->listen_error_code = 0; if (start_rdcw_watch(data, data->watch_worktree) == -1) goto force_error_stop; if (data->watch_gitdir && start_rdcw_watch(data, data->watch_gitdir) == -1) goto force_error_stop; for (;;) { dwWait = WaitForMultipleObjects(data->nr_listener_handles, data->hListener, FALSE, INFINITE); if (dwWait == WAIT_OBJECT_0 + LISTENER_HAVE_DATA_WORKTREE) { result = recv_rdcw_watch(data->watch_worktree); if (result == -1) { /* hard error */ goto force_error_stop; } if (result == -2) { /* retryable error */ if (start_rdcw_watch(data, data->watch_worktree) == -1) goto force_error_stop; continue; } /* have data */ if (process_worktree_events(state) == LISTENER_SHUTDOWN) goto force_shutdown; if (start_rdcw_watch(data, data->watch_worktree) == -1) goto force_error_stop; continue; } if (dwWait == WAIT_OBJECT_0 + LISTENER_HAVE_DATA_GITDIR) { result = recv_rdcw_watch(data->watch_gitdir); if (result == -1) { /* hard error */ goto force_error_stop; } if (result == -2) { /* retryable error */ if (start_rdcw_watch(data, data->watch_gitdir) == -1) goto force_error_stop; continue; } /* have data */ if (process_gitdir_events(state) == LISTENER_SHUTDOWN) goto force_shutdown; if (start_rdcw_watch(data, data->watch_gitdir) == -1) goto force_error_stop; continue; } if (dwWait == WAIT_OBJECT_0 + LISTENER_SHUTDOWN) goto clean_shutdown; error(_("could not read directory changes [GLE %ld]"), GetLastError()); goto force_error_stop; } force_error_stop: state->listen_error_code = -1; force_shutdown: /* * Tell the IPC thead pool to stop (which completes the await * in the main thread (which will also signal this thread (if * we are still alive))). */ ipc_server_stop_async(state->ipc_server_data); clean_shutdown: cancel_rdcw_watch(data->watch_worktree); cancel_rdcw_watch(data->watch_gitdir); } int fsm_listen__ctor(struct fsmonitor_daemon_state *state) { struct fsm_listen_data *data; CALLOC_ARRAY(data, 1); data->hEventShutdown = CreateEvent(NULL, TRUE, FALSE, NULL); data->watch_worktree = create_watch(state, state->path_worktree_watch.buf); if (!data->watch_worktree) goto failed; check_for_shortnames(data->watch_worktree); if (state->nr_paths_watching > 1) { data->watch_gitdir = create_watch(state, state->path_gitdir_watch.buf); if (!data->watch_gitdir) goto failed; } data->hListener[LISTENER_SHUTDOWN] = data->hEventShutdown; data->nr_listener_handles++; data->hListener[LISTENER_HAVE_DATA_WORKTREE] = data->watch_worktree->hEvent; data->nr_listener_handles++; if (data->watch_gitdir) { data->hListener[LISTENER_HAVE_DATA_GITDIR] = data->watch_gitdir->hEvent; data->nr_listener_handles++; } state->listen_data = data; return 0; failed: CloseHandle(data->hEventShutdown); destroy_watch(data->watch_worktree); destroy_watch(data->watch_gitdir); return -1; } void fsm_listen__dtor(struct fsmonitor_daemon_state *state) { struct fsm_listen_data *data; if (!state || !state->listen_data) return; data = state->listen_data; CloseHandle(data->hEventShutdown); destroy_watch(data->watch_worktree); destroy_watch(data->watch_gitdir); FREE_AND_NULL(state->listen_data); }