| #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; |
| 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; |
| }; |
| |
| struct fsmonitor_daemon_backend_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 notification into UTF8 and |
| * then normalize it. |
| */ |
| static int normalize_path_in_utf8(FILE_NOTIFY_INFORMATION *info, |
| struct strbuf *normalized_path) |
| { |
| int reserve; |
| int len = 0; |
| |
| strbuf_reset(normalized_path); |
| if (!info->FileNameLength) |
| 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 = info->FileNameLength + 1; |
| strbuf_grow(normalized_path, reserve); |
| |
| for (;;) { |
| len = WideCharToMultiByte(CP_UTF8, 0, info->FileName, |
| info->FileNameLength / sizeof(WCHAR), |
| 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)(info->FileNameLength / sizeof(WCHAR)), |
| info->FileName); |
| return -1; |
| } |
| |
| strbuf_grow(normalized_path, |
| strbuf_avail(normalized_path) + reserve); |
| } |
| |
| normalize: |
| strbuf_setlen(normalized_path, len); |
| return strbuf_normalize_path(normalized_path); |
| } |
| |
| void fsm_listen__stop_async(struct fsmonitor_daemon_state *state) |
| { |
| SetEvent(state->backend_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; |
| wchar_t wpath[MAX_PATH]; |
| |
| 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; |
| } |
| |
| 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); |
| |
| 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 fsmonitor_daemon_backend_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; |
| } |
| |
| /* |
| * NEEDSWORK: If an external <gitdir> is deleted, the above |
| * returns an error. I'm not sure that there's anything that |
| * we can do here other than failing -- the <worktree>/.git |
| * link file would be broken anyway. We might try to check |
| * for that and return a better error message, but I'm not |
| * sure it is worth it. |
| */ |
| |
| 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 filesystem events that happen anywhere (recursively) under the |
| * <worktree> root directory. For a normal working directory, this includes |
| * both version controlled files and the contents of the .git/ directory. |
| * |
| * If <worktree>/.git is a file, then we only see events for the file |
| * itself. |
| */ |
| static int process_worktree_events(struct fsmonitor_daemon_state *state) |
| { |
| struct fsmonitor_daemon_backend_data *data = state->backend_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; |
| |
| /* |
| * 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; |
| const char *slash; |
| enum fsmonitor_path_type t; |
| |
| strbuf_reset(&path); |
| if (normalize_path_in_utf8(info, &path) == -1) |
| goto skip_this_path; |
| |
| t = fsmonitor_classify_path_workdir_relative(path.buf); |
| |
| 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 "<worktree>/.git/" */ |
| break; |
| |
| case IS_DOT_GIT: |
| /* "<worktree>/.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"); |
| goto force_shutdown; |
| } |
| 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); |
| } |
| |
| 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 <gitdir> (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 <gitdir> |
| * itself (it is not inside something that we are watching). In particular, |
| * we do not get an event if the external <gitdir> is deleted. |
| */ |
| static int process_gitdir_events(struct fsmonitor_daemon_state *state) |
| { |
| struct fsmonitor_daemon_backend_data *data = state->backend_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; |
| |
| strbuf_reset(&path); |
| if (normalize_path_in_utf8(info, &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 fsmonitor_daemon_backend_data *data = state->backend_data; |
| DWORD dwWait; |
| int result; |
| |
| state->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->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 fsmonitor_daemon_backend_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; |
| |
| 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->backend_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 fsmonitor_daemon_backend_data *data; |
| |
| if (!state || !state->backend_data) |
| return; |
| |
| data = state->backend_data; |
| |
| CloseHandle(data->hEventShutdown); |
| destroy_watch(data->watch_worktree); |
| destroy_watch(data->watch_gitdir); |
| |
| FREE_AND_NULL(state->backend_data); |
| } |