Browse Source

Merge branch 'jh/builtin-fsmonitor-part3'

More fsmonitor--daemon.

* jh/builtin-fsmonitor-part3: (30 commits)
  t7527: improve implicit shutdown testing in fsmonitor--daemon
  fsmonitor--daemon: allow --super-prefix argument
  t7527: test Unicode NFC/NFD handling on MacOS
  t/lib-unicode-nfc-nfd: helper prereqs for testing unicode nfc/nfd
  t/helper/hexdump: add helper to print hexdump of stdin
  fsmonitor: on macOS also emit NFC spelling for NFD pathname
  t7527: test FSMonitor on case insensitive+preserving file system
  fsmonitor: never set CE_FSMONITOR_VALID on submodules
  t/perf/p7527: add perf test for builtin FSMonitor
  t7527: FSMonitor tests for directory moves
  fsmonitor: optimize processing of directory events
  fsm-listen-darwin: shutdown daemon if worktree root is moved/renamed
  fsm-health-win32: force shutdown daemon if worktree root moves
  fsm-health-win32: add polling framework to monitor daemon health
  fsmonitor--daemon: stub in health thread
  fsmonitor--daemon: rename listener thread related variables
  fsmonitor--daemon: prepare for adding health thread
  fsmonitor--daemon: cd out of worktree root
  fsm-listen-darwin: ignore FSEvents caused by xattr changes on macOS
  unpack-trees: initialize fsmonitor_has_run_once in o->result
  ...
pull/1271/merge
Junio C Hamano 2 months ago
parent
commit
9e496fffc8
  1. 20
      Makefile
  2. 116
      builtin/fsmonitor--daemon.c
  3. 16
      builtin/update-index.c
  4. 24
      compat/fsmonitor/fsm-health-darwin.c
  5. 278
      compat/fsmonitor/fsm-health-win32.c
  6. 47
      compat/fsmonitor/fsm-health.h
  7. 122
      compat/fsmonitor/fsm-listen-darwin.c
  8. 436
      compat/fsmonitor/fsm-listen-win32.c
  9. 2
      compat/fsmonitor/fsm-listen.h
  10. 89
      compat/fsmonitor/fsm-settings-darwin.c
  11. 137
      compat/fsmonitor/fsm-settings-win32.c
  12. 5
      config.mak.uname
  13. 8
      contrib/buildsystems/CMakeLists.txt
  14. 11
      fsmonitor--daemon.h
  15. 167
      fsmonitor-settings.c
  16. 33
      fsmonitor-settings.h
  17. 73
      fsmonitor.c
  18. 11
      fsmonitor.h
  19. 2
      git.c
  20. 106
      t/helper/test-fsmonitor-client.c
  21. 30
      t/helper/test-hexdump.c
  22. 1
      t/helper/test-tool.c
  23. 1
      t/helper/test-tool.h
  24. 162
      t/lib-unicode-nfc-nfd.sh
  25. 257
      t/perf/p7527-builtin-fsmonitor.sh
  26. 32
      t/t7519-status-fsmonitor.sh
  27. 401
      t/t7527-builtin-fsmonitor.sh
  28. 1
      unpack-trees.c

20
Makefile

@ -477,8 +477,14 @@ include shared.mak
#
# If your platform supports a built-in fsmonitor backend, set
# FSMONITOR_DAEMON_BACKEND to the "<name>" of the corresponding
# `compat/fsmonitor/fsm-listen-<name>.c` that implements the
# `fsm_listen__*()` routines.
# `compat/fsmonitor/fsm-listen-<name>.c` and
# `compat/fsmonitor/fsm-health-<name>.c` files
# that implement the `fsm_listen__*()` and `fsm_health__*()` routines.
#
# If your platform has OS-specific ways to tell if a repo is incompatible with
# fsmonitor (whether the hook or IPC daemon version), set FSMONITOR_OS_SETTINGS
# to the "<name>" of the corresponding `compat/fsmonitor/fsm-settings-<name>.c`
# that implements the `fsm_os_settings__*()` routines.
#
# Define DEVELOPER to enable more compiler warnings. Compiler version
# and family are auto detected, but could be overridden by defining
@ -730,6 +736,7 @@ TEST_BUILTINS_OBJS += test-getcwd.o
TEST_BUILTINS_OBJS += test-hash-speed.o
TEST_BUILTINS_OBJS += test-hash.o
TEST_BUILTINS_OBJS += test-hashmap.o
TEST_BUILTINS_OBJS += test-hexdump.o
TEST_BUILTINS_OBJS += test-index-version.o
TEST_BUILTINS_OBJS += test-json-writer.o
TEST_BUILTINS_OBJS += test-lazy-init-name-hash.o
@ -2012,6 +2019,12 @@ endif
ifdef FSMONITOR_DAEMON_BACKEND
COMPAT_CFLAGS += -DHAVE_FSMONITOR_DAEMON_BACKEND
COMPAT_OBJS += compat/fsmonitor/fsm-listen-$(FSMONITOR_DAEMON_BACKEND).o
COMPAT_OBJS += compat/fsmonitor/fsm-health-$(FSMONITOR_DAEMON_BACKEND).o
endif
ifdef FSMONITOR_OS_SETTINGS
COMPAT_CFLAGS += -DHAVE_FSMONITOR_OS_SETTINGS
COMPAT_OBJS += compat/fsmonitor/fsm-settings-$(FSMONITOR_OS_SETTINGS).o
endif
ifeq ($(TCLTK_PATH),)
@ -2966,6 +2979,9 @@ GIT-BUILD-OPTIONS: FORCE
ifdef FSMONITOR_DAEMON_BACKEND
@echo FSMONITOR_DAEMON_BACKEND=\''$(subst ','\'',$(subst ','\'',$(FSMONITOR_DAEMON_BACKEND)))'\' >>$@+
endif
ifdef FSMONITOR_OS_SETTINGS
@echo FSMONITOR_OS_SETTINGS=\''$(subst ','\'',$(subst ','\'',$(FSMONITOR_OS_SETTINGS)))'\' >>$@+
endif
ifdef TEST_OUTPUT_DIRECTORY
@echo TEST_OUTPUT_DIRECTORY=\''$(subst ','\'',$(subst ','\'',$(TEST_OUTPUT_DIRECTORY)))'\' >>$@+
endif

116
builtin/fsmonitor--daemon.c

@ -3,6 +3,7 @@
#include "parse-options.h"
#include "fsmonitor.h"
#include "fsmonitor-ipc.h"
#include "compat/fsmonitor/fsm-health.h"
#include "compat/fsmonitor/fsm-listen.h"
#include "fsmonitor--daemon.h"
#include "simple-ipc.h"
@ -1136,6 +1137,18 @@ void fsmonitor_publish(struct fsmonitor_daemon_state *state,
pthread_mutex_unlock(&state->main_lock);
}
static void *fsm_health__thread_proc(void *_state)
{
struct fsmonitor_daemon_state *state = _state;
trace2_thread_start("fsm-health");
fsm_health__loop(state);
trace2_thread_exit();
return NULL;
}
static void *fsm_listen__thread_proc(void *_state)
{
struct fsmonitor_daemon_state *state = _state;
@ -1174,6 +1187,9 @@ static int fsmonitor_run_daemon_1(struct fsmonitor_daemon_state *state)
*/
.uds_disallow_chdir = 0
};
int health_started = 0;
int listener_started = 0;
int err = 0;
/*
* Start the IPC thread pool before the we've started the file
@ -1181,11 +1197,11 @@ static int fsmonitor_run_daemon_1(struct fsmonitor_daemon_state *state)
* before we need it.
*/
if (ipc_server_run_async(&state->ipc_server_data,
fsmonitor_ipc__get_path(), &ipc_opts,
state->path_ipc.buf, &ipc_opts,
handle_client, state))
return error_errno(
_("could not start IPC thread pool on '%s'"),
fsmonitor_ipc__get_path());
state->path_ipc.buf);
/*
* Start the fsmonitor listener thread to collect filesystem
@ -1194,15 +1210,31 @@ static int fsmonitor_run_daemon_1(struct fsmonitor_daemon_state *state)
if (pthread_create(&state->listener_thread, NULL,
fsm_listen__thread_proc, state) < 0) {
ipc_server_stop_async(state->ipc_server_data);
ipc_server_await(state->ipc_server_data);
err = error(_("could not start fsmonitor listener thread"));
goto cleanup;
}
listener_started = 1;
return error(_("could not start fsmonitor listener thread"));
/*
* Start the health thread to watch over our process.
*/
if (pthread_create(&state->health_thread, NULL,
fsm_health__thread_proc, state) < 0) {
ipc_server_stop_async(state->ipc_server_data);
err = error(_("could not start fsmonitor health thread"));
goto cleanup;
}
health_started = 1;
/*
* The daemon is now fully functional in background threads.
* Our primary thread should now just wait while the threads
* do all the work.
*/
cleanup:
/*
* Wait for the IPC thread pool to shutdown (whether by client
* request or from filesystem activity).
* request, from filesystem activity, or an error).
*/
ipc_server_await(state->ipc_server_data);
@ -1211,15 +1243,29 @@ static int fsmonitor_run_daemon_1(struct fsmonitor_daemon_state *state)
* event from the IPC thread pool, but it doesn't hurt to tell
* it again. And wait for it to shutdown.
*/
fsm_listen__stop_async(state);
pthread_join(state->listener_thread, NULL);
if (listener_started) {
fsm_listen__stop_async(state);
pthread_join(state->listener_thread, NULL);
}
return state->error_code;
if (health_started) {
fsm_health__stop_async(state);
pthread_join(state->health_thread, NULL);
}
if (err)
return err;
if (state->listen_error_code)
return state->listen_error_code;
if (state->health_error_code)
return state->health_error_code;
return 0;
}
static int fsmonitor_run_daemon(void)
{
struct fsmonitor_daemon_state state;
const char *home;
int err;
memset(&state, 0, sizeof(state));
@ -1227,7 +1273,8 @@ static int fsmonitor_run_daemon(void)
hashmap_init(&state.cookies, cookies_cmp, NULL, 0);
pthread_mutex_init(&state.main_lock, NULL);
pthread_cond_init(&state.cookies_cond, NULL);
state.error_code = 0;
state.listen_error_code = 0;
state.health_error_code = 0;
state.current_token_data = fsmonitor_new_token_data();
/* Prepare to (recursively) watch the <worktree-root> directory. */
@ -1289,6 +1336,15 @@ static int fsmonitor_run_daemon(void)
strbuf_addch(&state.path_cookie_prefix, '/');
/*
* We create a named-pipe or unix domain socket inside of the
* ".git" directory. (Well, on Windows, we base our named
* pipe in the NPFS on the absolute path of the git
* directory.)
*/
strbuf_init(&state.path_ipc, 0);
strbuf_addstr(&state.path_ipc, absolute_path(fsmonitor_ipc__get_path()));
/*
* Confirm that we can create platform-specific resources for the
* filesystem listener before we bother starting all the threads.
@ -1298,18 +1354,42 @@ static int fsmonitor_run_daemon(void)
goto done;
}
if (fsm_health__ctor(&state)) {
err = error(_("could not initialize health thread"));
goto done;
}
/*
* CD out of the worktree root directory.
*
* The common Git startup mechanism causes our CWD to be the
* root of the worktree. On Windows, this causes our process
* to hold a locked handle on the CWD. This prevents the
* worktree from being moved or deleted while the daemon is
* running.
*
* We assume that our FS and IPC listener threads have either
* opened all of the handles that they need or will do
* everything using absolute paths.
*/
home = getenv("HOME");
if (home && *home && chdir(home))
die_errno(_("could not cd home '%s'"), home);
err = fsmonitor_run_daemon_1(&state);
done:
pthread_cond_destroy(&state.cookies_cond);
pthread_mutex_destroy(&state.main_lock);
fsm_listen__dtor(&state);
fsm_health__dtor(&state);
ipc_server_free(state.ipc_server_data);
strbuf_release(&state.path_worktree_watch);
strbuf_release(&state.path_gitdir_watch);
strbuf_release(&state.path_cookie_prefix);
strbuf_release(&state.path_ipc);
return err;
}
@ -1423,6 +1503,7 @@ static int try_to_start_background_daemon(void)
int cmd_fsmonitor__daemon(int argc, const char **argv, const char *prefix)
{
const char *subcmd;
enum fsmonitor_reason reason;
int detach_console = 0;
struct option options[] = {
@ -1449,6 +1530,23 @@ int cmd_fsmonitor__daemon(int argc, const char **argv, const char *prefix)
die(_("invalid 'ipc-threads' value (%d)"),
fsmonitor__ipc_threads);
prepare_repo_settings(the_repository);
/*
* If the repo is fsmonitor-compatible, explicitly set IPC-mode
* (without bothering to load the `core.fsmonitor` config settings).
*
* If the repo is not compatible, the repo-settings will be set to
* incompatible rather than IPC, so we can use one of the __get
* routines to detect the discrepancy.
*/
fsm_settings__set_ipc(the_repository);
reason = fsm_settings__get_reason(the_repository);
if (reason > FSMONITOR_REASON_OK)
die("%s",
fsm_settings__get_incompatible_msg(the_repository,
reason));
if (!strcmp(subcmd, "start"))
return !!try_to_start_background_daemon();

16
builtin/update-index.c

@ -1257,6 +1257,22 @@ int cmd_update_index(int argc, const char **argv, const char *prefix)
if (fsmonitor > 0) {
enum fsmonitor_mode fsm_mode = fsm_settings__get_mode(r);
enum fsmonitor_reason reason = fsm_settings__get_reason(r);
/*
* The user wants to turn on FSMonitor using the command
* line argument. (We don't know (or care) whether that
* is the IPC or HOOK version.)
*
* Use one of the __get routines to force load the FSMonitor
* config settings into the repo-settings. That will detect
* whether the file system is compatible so that we can stop
* here with a nice error message.
*/
if (reason > FSMONITOR_REASON_OK)
die("%s",
fsm_settings__get_incompatible_msg(r, reason));
if (fsm_mode == FSMONITOR_MODE_DISABLED) {
warning(_("core.fsmonitor is unset; "
"set it if you really want to "

24
compat/fsmonitor/fsm-health-darwin.c

@ -0,0 +1,24 @@
#include "cache.h"
#include "config.h"
#include "fsmonitor.h"
#include "fsm-health.h"
#include "fsmonitor--daemon.h"
int fsm_health__ctor(struct fsmonitor_daemon_state *state)
{
return 0;
}
void fsm_health__dtor(struct fsmonitor_daemon_state *state)
{
return;
}
void fsm_health__loop(struct fsmonitor_daemon_state *state)
{
return;
}
void fsm_health__stop_async(struct fsmonitor_daemon_state *state)
{
}

278
compat/fsmonitor/fsm-health-win32.c

@ -0,0 +1,278 @@
#include "cache.h"
#include "config.h"
#include "fsmonitor.h"
#include "fsm-health.h"
#include "fsmonitor--daemon.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_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;
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]);
}

47
compat/fsmonitor/fsm-health.h

@ -0,0 +1,47 @@
#ifndef FSM_HEALTH_H
#define FSM_HEALTH_H
/* This needs to be implemented by each backend */
#ifdef HAVE_FSMONITOR_DAEMON_BACKEND
struct fsmonitor_daemon_state;
/*
* Initialize platform-specific data for the fsmonitor health thread.
* This will be called from the main thread PRIOR to staring the
* thread.
*
* Returns 0 if successful.
* Returns -1 otherwise.
*/
int fsm_health__ctor(struct fsmonitor_daemon_state *state);
/*
* Cleanup platform-specific data for the health thread.
* This will be called from the main thread AFTER joining the thread.
*/
void fsm_health__dtor(struct fsmonitor_daemon_state *state);
/*
* The main body of the platform-specific event loop to monitor the
* health of the daemon process. This will run in the health thread.
*
* The health thread should call `ipc_server_stop_async()` if it needs
* to cause a shutdown. (It should NOT do so if it receives a shutdown
* shutdown signal.)
*
* It should set `state->health_error_code` to -1 if the daemon should exit
* with an error.
*/
void fsm_health__loop(struct fsmonitor_daemon_state *state);
/*
* Gently request that the health thread shutdown.
* It does not wait for it to stop. The caller should do a JOIN
* to wait for it.
*/
void fsm_health__stop_async(struct fsmonitor_daemon_state *state);
#endif /* HAVE_FSMONITOR_DAEMON_BACKEND */
#endif /* FSM_HEALTH_H */

122
compat/fsmonitor/fsm-listen-darwin.c

@ -27,7 +27,7 @@
#include "fsm-listen.h"
#include "fsmonitor--daemon.h"
struct fsmonitor_daemon_backend_data
struct fsm_listen_data
{
CFStringRef cfsr_worktree_path;
CFStringRef cfsr_gitdir_path;
@ -100,12 +100,17 @@ static void log_flags_set(const char *path, const FSEventStreamEventFlags flag)
if (flag & kFSEventStreamEventFlagItemCloned)
strbuf_addstr(&msg, "ItemCloned|");
trace_printf_key(&trace_fsmonitor, "fsevent: '%s', flags=%u %s",
trace_printf_key(&trace_fsmonitor, "fsevent: '%s', flags=0x%x %s",
path, flag, msg.buf);
strbuf_release(&msg);
}
static int ef_is_root_changed(const FSEventStreamEventFlags ef)
{
return (ef & kFSEventStreamEventFlagRootChanged);
}
static int ef_is_root_delete(const FSEventStreamEventFlags ef)
{
return (ef & kFSEventStreamEventFlagItemIsDir &&
@ -125,6 +130,60 @@ static int ef_is_dropped(const FSEventStreamEventFlags ef)
ef & kFSEventStreamEventFlagUserDropped);
}
/*
* If an `xattr` change is the only reason we received this event,
* then silently ignore it. Git doesn't care about xattr's. We
* have to be careful here because the kernel can combine multiple
* events for a single path. And because events always have certain
* bits set, such as `ItemIsFile` or `ItemIsDir`.
*
* Return 1 if we should ignore it.
*/
static int ef_ignore_xattr(const FSEventStreamEventFlags ef)
{
static const FSEventStreamEventFlags mask =
kFSEventStreamEventFlagItemChangeOwner |
kFSEventStreamEventFlagItemCreated |
kFSEventStreamEventFlagItemFinderInfoMod |
kFSEventStreamEventFlagItemInodeMetaMod |
kFSEventStreamEventFlagItemModified |
kFSEventStreamEventFlagItemRemoved |
kFSEventStreamEventFlagItemRenamed |
kFSEventStreamEventFlagItemXattrMod |
kFSEventStreamEventFlagItemCloned;
return ((ef & mask) == kFSEventStreamEventFlagItemXattrMod);
}
/*
* On MacOS we have to adjust for Unicode composition insensitivity
* (where NFC and NFD spellings are not respected). The different
* spellings are essentially aliases regardless of how the path is
* actually stored on the disk.
*
* This is related to "core.precomposeUnicode" (which wants to try
* to hide NFD completely and treat everything as NFC). Here, we
* don't know what the value the client has (or will have) for this
* config setting when they make a query, so assume the worst and
* emit both when the OS gives us an NFD path.
*/
static void my_add_path(struct fsmonitor_batch *batch, const char *path)
{
char *composed;
/* add the NFC or NFD path as received from the OS */
fsmonitor_batch__add_path(batch, path);
/* if NFD, also add the corresponding NFC spelling */
composed = (char *)precompose_string_if_needed(path);
if (!composed || composed == path)
return;
fsmonitor_batch__add_path(batch, composed);
free(composed);
}
static void fsevent_callback(ConstFSEventStreamRef streamRef,
void *ctx,
size_t num_of_events,
@ -133,7 +192,7 @@ static void fsevent_callback(ConstFSEventStreamRef streamRef,
const FSEventStreamEventId event_ids[])
{
struct fsmonitor_daemon_state *state = ctx;
struct fsmonitor_daemon_backend_data *data = state->backend_data;
struct fsm_listen_data *data = state->listen_data;
char **paths = (char **)event_paths;
struct fsmonitor_batch *batch = NULL;
struct string_list cookie_list = STRING_LIST_INIT_DUP;
@ -190,6 +249,33 @@ static void fsevent_callback(ConstFSEventStreamRef streamRef,
continue;
}
if (ef_is_root_changed(event_flags[k])) {
/*
* The spelling of the pathname of the root directory
* has changed. This includes the name of the root
* directory itself or of any parent directory in the
* path.
*
* (There may be other conditions that throw this,
* but I couldn't find any information on it.)
*
* Force a shutdown now and avoid things getting
* out of sync. The Unix domain socket is inside
* the .git directory and a spelling change will make
* it hard for clients to rendezvous with us.
*/
trace_printf_key(&trace_fsmonitor,
"event: root changed");
goto force_shutdown;
}
if (ef_ignore_xattr(event_flags[k])) {
trace_printf_key(&trace_fsmonitor,
"ignore-xattr: '%s', flags=0x%x",
path_k, event_flags[k]);
continue;
}
switch (fsmonitor_classify_path_absolute(state, path_k)) {
case IS_INSIDE_DOT_GIT_WITH_COOKIE_PREFIX:
@ -248,7 +334,7 @@ static void fsevent_callback(ConstFSEventStreamRef streamRef,
if (!batch)
batch = fsmonitor_batch__new();
fsmonitor_batch__add_path(batch, rel);
my_add_path(batch, rel);
}
if (event_flags[k] & kFSEventStreamEventFlagItemIsDir) {
@ -261,7 +347,7 @@ static void fsevent_callback(ConstFSEventStreamRef streamRef,
if (!batch)
batch = fsmonitor_batch__new();
fsmonitor_batch__add_path(batch, tmp.buf);
my_add_path(batch, tmp.buf);
}
break;
@ -318,11 +404,11 @@ int fsm_listen__ctor(struct fsmonitor_daemon_state *state)
NULL,
NULL
};
struct fsmonitor_daemon_backend_data *data;
struct fsm_listen_data *data;
const void *dir_array[2];
CALLOC_ARRAY(data, 1);
state->backend_data = data;
state->listen_data = data;
data->cfsr_worktree_path = CFStringCreateWithCString(
NULL, state->path_worktree_watch.buf, kCFStringEncodingUTF8);
@ -354,18 +440,18 @@ int fsm_listen__ctor(struct fsmonitor_daemon_state *state)
failed:
error(_("Unable to create FSEventStream."));
FREE_AND_NULL(state->backend_data);
FREE_AND_NULL(state->listen_data);
return -1;
}
void fsm_listen__dtor(struct fsmonitor_daemon_state *state)
{
struct fsmonitor_daemon_backend_data *data;
struct fsm_listen_data *data;
if (!state || !state->backend_data)
if (!state || !state->listen_data)
return;
data = state->backend_data;
data = state->listen_data;
if (data->stream) {
if (data->stream_started)
@ -375,14 +461,14 @@ void fsm_listen__dtor(struct fsmonitor_daemon_state *state)
FSEventStreamRelease(data->stream);
}
FREE_AND_NULL(state->backend_data);
FREE_AND_NULL(state->listen_data);
}
void fsm_listen__stop_async(struct fsmonitor_daemon_state *state)
{
struct fsmonitor_daemon_backend_data *data;
struct fsm_listen_data *data;
data = state->backend_data;
data = state->listen_data;
data->shutdown_style = SHUTDOWN_EVENT;
CFRunLoopStop(data->rl);
@ -390,9 +476,9 @@ void fsm_listen__stop_async(struct fsmonitor_daemon_state *state)
void fsm_listen__loop(struct fsmonitor_daemon_state *state)
{
struct fsmonitor_daemon_backend_data *data;
struct fsm_listen_data *data;
data = state->backend_data;
data = state->listen_data;
data->rl = CFRunLoopGetCurrent();
@ -409,7 +495,7 @@ void fsm_listen__loop(struct fsmonitor_daemon_state *state)
switch (data->shutdown_style) {
case FORCE_ERROR_STOP:
state->error_code = -1;
state->listen_error_code = -1;
/* fall thru */
case FORCE_SHUTDOWN:
ipc_server_stop_async(state->ipc_server_data);
@ -421,7 +507,7 @@ void fsm_listen__loop(struct fsmonitor_daemon_state *state)
return;
force_error_stop_without_loop:
state->error_code = -1;
state->listen_error_code = -1;
ipc_server_stop_async(state->ipc_server_data);
return;
}

436
compat/fsmonitor/fsm-listen-win32.c

@ -25,6 +25,9 @@ struct one_watch
DWORD count;
struct strbuf path;
wchar_t wpath_longname[MAX_PATH + 1];
DWORD wpath_longname_len;
HANDLE hDir;
HANDLE hEvent;
OVERLAPPED overlapped;
@ -34,9 +37,24 @@ struct one_watch
* 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 fsmonitor_daemon_backend_data
struct fsm_listen_data
{
struct one_watch *watch_worktree;
struct one_watch *watch_gitdir;
@ -51,17 +69,18 @@ struct fsmonitor_daemon_backend_data
};
/*
* Convert the WCHAR path from the notification into UTF8 and
* then normalize it.
* 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(FILE_NOTIFY_INFORMATION *info,
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 (!info->FileNameLength)
if (!wpath_len)
goto normalize;
/*
@ -70,12 +89,12 @@ static int normalize_path_in_utf8(FILE_NOTIFY_INFORMATION *info,
* sequence of 2 UTF8 characters. That should let us
* avoid ERROR_INSUFFICIENT_BUFFER 99.9+% of the time.
*/
reserve = info->FileNameLength + 1;
reserve = 2 * wpath_len + 1;
strbuf_grow(normalized_path, reserve);
for (;;) {
len = WideCharToMultiByte(CP_UTF8, 0, info->FileName,
info->FileNameLength / sizeof(WCHAR),
len = WideCharToMultiByte(CP_UTF8, 0,
wpath, wpath_len,
normalized_path->buf,
strbuf_avail(normalized_path) - 1,
NULL, NULL);
@ -83,9 +102,7 @@ static int normalize_path_in_utf8(FILE_NOTIFY_INFORMATION *info,
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);
GetLastError(), (int)wpath_len, wpath);
return -1;
}
@ -98,9 +115,176 @@ normalize:
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"<wt-root-path>/.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"<wt-root-path>/<event-rel-path>"
* Note that the <event-rel-path> 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->backend_data->hListener[LISTENER_SHUTDOWN]);
SetEvent(state->listen_data->hListener[LISTENER_SHUTDOWN]);
}
static struct one_watch *create_watch(struct fsmonitor_daemon_state *state,
@ -111,7 +295,9 @@ static struct one_watch *create_watch(struct fsmonitor_daemon_state *state,
DWORD share_mode =
FILE_SHARE_WRITE | FILE_SHARE_READ | FILE_SHARE_DELETE;
HANDLE hDir;
wchar_t wpath[MAX_PATH];
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);
@ -128,6 +314,21 @@ static struct one_watch *create_watch(struct fsmonitor_daemon_state *state,
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 */
@ -135,6 +336,9 @@ static struct one_watch *create_watch(struct fsmonitor_daemon_state *state,
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);
@ -155,7 +359,7 @@ static void destroy_watch(struct one_watch *watch)
free(watch);
}
static int start_rdcw_watch(struct fsmonitor_daemon_backend_data *data,
static int start_rdcw_watch(struct fsm_listen_data *data,
struct one_watch *watch)
{
DWORD dwNotifyFilter =
@ -220,12 +424,22 @@ static int recv_rdcw_watch(struct one_watch *watch)
}
/*
* 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.
* 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]"),
@ -258,6 +472,62 @@ static void cancel_rdcw_watch(struct one_watch *watch)
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 "<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");
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
* <worktree> root directory. For a normal working directory, this includes
@ -268,12 +538,13 @@ static void cancel_rdcw_watch(struct one_watch *watch)
*/
static int process_worktree_events(struct fsmonitor_daemon_state *state)
{
struct fsmonitor_daemon_backend_data *data = state->backend_data;
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<