mirror of
https://github.com/git-for-windows/git.git
synced 2026-03-26 03:51:40 -05:00
Merge branch 'try-v4-fsmonitor-part4' into try-v4-fsmonitor
This commit is contained in:
19
Makefile
19
Makefile
@@ -466,8 +466,14 @@ all::
|
||||
#
|
||||
# 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
|
||||
@@ -1947,6 +1953,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),)
|
||||
@@ -2878,6 +2890,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
|
||||
|
||||
@@ -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"
|
||||
@@ -27,6 +28,9 @@ static int fsmonitor__ipc_threads = 8;
|
||||
#define FSMONITOR__START_TIMEOUT "fsmonitor.starttimeout"
|
||||
static int fsmonitor__start_timeout_sec = 60;
|
||||
|
||||
#define FSMONITOR__ANNOUNCE_STARTUP "fsmonitor.announcestartup"
|
||||
static int fsmonitor__announce_startup = 0;
|
||||
|
||||
static int fsmonitor_config(const char *var, const char *value, void *cb)
|
||||
{
|
||||
if (!strcmp(var, FSMONITOR__IPC_THREADS)) {
|
||||
@@ -47,6 +51,16 @@ static int fsmonitor_config(const char *var, const char *value, void *cb)
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!strcmp(var, FSMONITOR__ANNOUNCE_STARTUP)) {
|
||||
int is_bool;
|
||||
int i = git_config_bool_or_int(var, value, &is_bool);
|
||||
if (i < 0)
|
||||
return error(_("value of '%s' not bool or int: %d"),
|
||||
var, i);
|
||||
fsmonitor__announce_startup = i;
|
||||
return 0;
|
||||
}
|
||||
|
||||
return git_default_config(var, value, cb);
|
||||
}
|
||||
|
||||
@@ -1111,6 +1125,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;
|
||||
@@ -1149,6 +1175,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
|
||||
@@ -1156,11 +1185,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
|
||||
@@ -1169,15 +1198,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);
|
||||
|
||||
return error(_("could not start fsmonitor listener thread"));
|
||||
err = error(_("could not start fsmonitor listener thread"));
|
||||
goto cleanup;
|
||||
}
|
||||
listener_started = 1;
|
||||
|
||||
/*
|
||||
* 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);
|
||||
|
||||
@@ -1186,15 +1231,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));
|
||||
@@ -1202,7 +1261,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. */
|
||||
@@ -1264,6 +1324,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.
|
||||
@@ -1273,18 +1342,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);
|
||||
|
||||
/*
|
||||
* NEEDSWORK: Consider "rm -rf <gitdir>/<fsmonitor-dir>"
|
||||
@@ -1307,9 +1400,11 @@ static int try_to_run_foreground_daemon(int free_console)
|
||||
die("fsmonitor--daemon is already running '%s'",
|
||||
the_repository->worktree);
|
||||
|
||||
printf(_("running fsmonitor-daemon in '%s'\n"),
|
||||
the_repository->worktree);
|
||||
fflush(stdout);
|
||||
if (fsmonitor__announce_startup) {
|
||||
fprintf(stderr, _("running fsmonitor-daemon in '%s'\n"),
|
||||
the_repository->worktree);
|
||||
fflush(stderr);
|
||||
}
|
||||
|
||||
#ifdef GIT_WINDOWS_NATIVE
|
||||
if (free_console)
|
||||
@@ -1360,9 +1455,11 @@ static int try_to_start_background_daemon(void)
|
||||
die("fsmonitor--daemon is already running '%s'",
|
||||
the_repository->worktree);
|
||||
|
||||
printf(_("starting fsmonitor-daemon in '%s'\n"),
|
||||
the_repository->worktree);
|
||||
fflush(stdout);
|
||||
if (fsmonitor__announce_startup) {
|
||||
fprintf(stderr, _("starting fsmonitor-daemon in '%s'\n"),
|
||||
the_repository->worktree);
|
||||
fflush(stderr);
|
||||
}
|
||||
|
||||
cp.git_cmd = 1;
|
||||
|
||||
@@ -1424,6 +1521,17 @@ 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);
|
||||
fsm_settings__set_ipc(the_repository);
|
||||
|
||||
if (fsm_settings__get_mode(the_repository) == FSMONITOR_MODE_INCOMPATIBLE) {
|
||||
struct strbuf buf_reason = STRBUF_INIT;
|
||||
fsm_settings__get_reason(the_repository, &buf_reason);
|
||||
error("%s '%s'", buf_reason.buf, xgetcwd());
|
||||
strbuf_release(&buf_reason);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!strcmp(subcmd, "start"))
|
||||
return !!try_to_start_background_daemon();
|
||||
|
||||
|
||||
@@ -1216,6 +1216,14 @@ 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);
|
||||
|
||||
if (fsm_mode == FSMONITOR_MODE_INCOMPATIBLE) {
|
||||
struct strbuf buf_reason = STRBUF_INIT;
|
||||
fsm_settings__get_reason(r, &buf_reason);
|
||||
error("%s", buf_reason.buf);
|
||||
strbuf_release(&buf_reason);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (fsm_mode == FSMONITOR_MODE_DISABLED) {
|
||||
warning(_("core.useBuiltinFSMonitor is unset; "
|
||||
"set it if you really want to enable the "
|
||||
|
||||
24
compat/fsmonitor/fsm-health-darwin.c
Normal file
24
compat/fsmonitor/fsm-health-darwin.c
Normal file
@@ -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)
|
||||
{
|
||||
}
|
||||
257
compat/fsmonitor/fsm-health-win32.c
Normal file
257
compat/fsmonitor/fsm-health-win32.c
Normal file
@@ -0,0 +1,257 @@
|
||||
#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)
|
||||
|
||||
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);
|
||||
|
||||
static interval_fn has_worktree_moved;
|
||||
|
||||
static interval_fn *table[] = {
|
||||
has_worktree_moved,
|
||||
NULL, /* must be last */
|
||||
};
|
||||
|
||||
/*
|
||||
* Call all of the 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;
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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
Normal file
47
compat/fsmonitor/fsm-health.h
Normal file
@@ -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 */
|
||||
@@ -99,7 +99,7 @@ void FSEventStreamRelease(FSEventStreamRef stream);
|
||||
#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;
|
||||
@@ -172,12 +172,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 &&
|
||||
@@ -197,6 +202,31 @@ 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);
|
||||
}
|
||||
|
||||
static void fsevent_callback(ConstFSEventStreamRef streamRef,
|
||||
void *ctx,
|
||||
size_t num_of_events,
|
||||
@@ -205,7 +235,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;
|
||||
@@ -262,6 +292,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 of 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:
|
||||
@@ -387,11 +444,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);
|
||||
@@ -423,18 +480,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)
|
||||
@@ -444,14 +501,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);
|
||||
@@ -459,9 +516,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();
|
||||
|
||||
@@ -478,7 +535,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);
|
||||
@@ -490,7 +547,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;
|
||||
}
|
||||
|
||||
@@ -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_tilda;
|
||||
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,151 @@ 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. (Out 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_slash = NULL;
|
||||
wchar_t *last_bslash = NULL;
|
||||
wchar_t *last;
|
||||
|
||||
/* build L"<wt-root-path>/.git" */
|
||||
wcscpy(buf_in, watch->wpath_longname);
|
||||
wcscpy(buf_in + watch->wpath_longname_len, L".git");
|
||||
|
||||
if (!GetShortPathNameW(buf_in, buf_out, MAX_PATH))
|
||||
return;
|
||||
|
||||
last_slash = wcsrchr(buf_out, L'/');
|
||||
last_bslash = wcsrchr(buf_out, L'\\');
|
||||
if (last_slash > last_bslash)
|
||||
last = last_slash + 1;
|
||||
else if (last_bslash)
|
||||
last = last_bslash + 1;
|
||||
else
|
||||
last = buf_out;
|
||||
|
||||
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_tilda = 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)
|
||||
{
|
||||
wchar_t buf_in[2 * MAX_PATH + 1];
|
||||
wchar_t buf_out[MAX_PATH + 1];
|
||||
DWORD root_len;
|
||||
|
||||
/* Build L"<wt-root-path>/<event-rel-path>" */
|
||||
root_len = watch->wpath_longname_len;
|
||||
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 routine allows either to be
|
||||
* given as input.
|
||||
*/
|
||||
if (!GetLongPathNameW(buf_in, buf_out, MAX_PATH)) {
|
||||
/*
|
||||
* 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.)
|
||||
*
|
||||
* NEEDSWORK: Since deleting or moving a file or
|
||||
* directory by shortname is rather obscure, I'm going
|
||||
* ignore the failure and ask the caller to report the
|
||||
* original relative path. This seemds kinder than
|
||||
* failing here and forcing a resync.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/* 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 +270,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 +289,20 @@ static struct one_watch *create_watch(struct fsmonitor_daemon_state *state,
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (!GetLongPathNameW(wpath, wpath_longname, MAX_PATH)) {
|
||||
error(_("[GLE %ld] could not get longname of '%s'"),
|
||||
GetLastError(), path);
|
||||
CloseHandle(hDir);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
len_longname = wcslen(wpath_longname);
|
||||
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 +310,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 +333,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 +398,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 +446,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 +512,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
|
||||
@@ -306,54 +551,63 @@ static int process_worktree_events(struct fsmonitor_daemon_state *state)
|
||||
*/
|
||||
for (;;) {
|
||||
FILE_NOTIFY_INFORMATION *info = (void *)p;
|
||||
const char *slash;
|
||||
wchar_t *wpath = info->FileName;
|
||||
DWORD wpath_len = info->FileNameLength / sizeof(WCHAR);
|
||||
enum fsmonitor_path_type t;
|
||||
enum get_relative_result grr;
|
||||
|
||||
strbuf_reset(&path);
|
||||
if (normalize_path_in_utf8(info, &path) == -1)
|
||||
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_tilda && !wcschr(wpath, L'~')) {
|
||||
/*
|
||||
* Shortnames on this filesystem have tildas
|
||||
* 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);
|
||||
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);
|
||||
|
||||
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);
|
||||
}
|
||||
process_it:
|
||||
if (process_1_worktree_event(&cookie_list, &batch, &path, t,
|
||||
info->Action))
|
||||
goto force_shutdown;
|
||||
|
||||
skip_this_path:
|
||||
if (!info->NextEntryOffset)
|
||||
@@ -382,10 +636,13 @@ force_shutdown:
|
||||
* 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.
|
||||
*
|
||||
* Also, we do not care about shortnames within the external <gitdir>, since
|
||||
* we never send these paths to clients.
|
||||
*/
|
||||
static int process_gitdir_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_gitdir;
|
||||
struct strbuf path = STRBUF_INIT;
|
||||
struct string_list cookie_list = STRING_LIST_INIT_DUP;
|
||||
@@ -403,8 +660,10 @@ static int process_gitdir_events(struct fsmonitor_daemon_state *state)
|
||||
const char *slash;
|
||||
enum fsmonitor_path_type t;
|
||||
|
||||
strbuf_reset(&path);
|
||||
if (normalize_path_in_utf8(info, &path) == -1)
|
||||
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);
|
||||
@@ -441,11 +700,11 @@ skip_this_path:
|
||||
|
||||
void fsm_listen__loop(struct fsmonitor_daemon_state *state)
|
||||
{
|
||||
struct fsmonitor_daemon_backend_data *data = state->backend_data;
|
||||
struct fsm_listen_data *data = state->listen_data;
|
||||
DWORD dwWait;
|
||||
int result;
|
||||
|
||||
state->error_code = 0;
|
||||
state->listen_error_code = 0;
|
||||
|
||||
if (start_rdcw_watch(data, data->watch_worktree) == -1)
|
||||
goto force_error_stop;
|
||||
@@ -510,7 +769,7 @@ void fsm_listen__loop(struct fsmonitor_daemon_state *state)
|
||||
}
|
||||
|
||||
force_error_stop:
|
||||
state->error_code = -1;
|
||||
state->listen_error_code = -1;
|
||||
|
||||
force_shutdown:
|
||||
/*
|
||||
@@ -527,7 +786,7 @@ clean_shutdown:
|
||||
|
||||
int fsm_listen__ctor(struct fsmonitor_daemon_state *state)
|
||||
{
|
||||
struct fsmonitor_daemon_backend_data *data;
|
||||
struct fsm_listen_data *data;
|
||||
|
||||
CALLOC_ARRAY(data, 1);
|
||||
|
||||
@@ -538,6 +797,8 @@ int fsm_listen__ctor(struct fsmonitor_daemon_state *state)
|
||||
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);
|
||||
@@ -558,7 +819,7 @@ int fsm_listen__ctor(struct fsmonitor_daemon_state *state)
|
||||
data->nr_listener_handles++;
|
||||
}
|
||||
|
||||
state->backend_data = data;
|
||||
state->listen_data = data;
|
||||
return 0;
|
||||
|
||||
failed:
|
||||
@@ -571,16 +832,16 @@ failed:
|
||||
|
||||
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;
|
||||
|
||||
CloseHandle(data->hEventShutdown);
|
||||
destroy_watch(data->watch_worktree);
|
||||
destroy_watch(data->watch_gitdir);
|
||||
|
||||
FREE_AND_NULL(state->backend_data);
|
||||
FREE_AND_NULL(state->listen_data);
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ void fsm_listen__dtor(struct fsmonitor_daemon_state *state);
|
||||
* do so if the listener thread receives a normal shutdown signal from
|
||||
* the IPC layer.)
|
||||
*
|
||||
* It should set `state->error_code` to -1 if the daemon should exit
|
||||
* It should set `state->listen_error_code` to -1 if the daemon should exit
|
||||
* with an error.
|
||||
*/
|
||||
void fsm_listen__loop(struct fsmonitor_daemon_state *state);
|
||||
|
||||
75
compat/fsmonitor/fsm-settings-darwin.c
Normal file
75
compat/fsmonitor/fsm-settings-darwin.c
Normal file
@@ -0,0 +1,75 @@
|
||||
#include "cache.h"
|
||||
#include "config.h"
|
||||
#include "repository.h"
|
||||
#include "fsmonitor-settings.h"
|
||||
#include "fsmonitor.h"
|
||||
#include <sys/param.h>
|
||||
#include <sys/mount.h>
|
||||
|
||||
/*
|
||||
* Remote working directories are problematic for FSMonitor.
|
||||
*
|
||||
* The underlying file system on the server machine and/or the remote
|
||||
* mount type (NFS, SAMBA, etc.) dictates whether notification events
|
||||
* are available at all to remote client machines.
|
||||
*
|
||||
* Kernel differences between the server and client machines also
|
||||
* dictate the how (buffering, frequency, de-dup) the events are
|
||||
* delivered to client machine processes.
|
||||
*
|
||||
* A client machine (such as a laptop) may choose to suspend/resume
|
||||
* and it is unclear (without lots of testing) whether the watcher can
|
||||
* resync after a resume. We might be able to treat this as a normal
|
||||
* "events were dropped by the kernel" event and do our normal "flush
|
||||
* and resync" --or-- we might need to close the existing (zombie?)
|
||||
* notification fd and create a new one.
|
||||
*
|
||||
* In theory, the above issues need to be addressed whether we are
|
||||
* using the Hook or IPC API.
|
||||
*
|
||||
* For the builtin FSMonitor, we create the Unix domain socket for the
|
||||
* IPC in the .git directory. If the working directory is remote,
|
||||
* then the socket will be created on the remote file system. This
|
||||
* can fail if the remote file system does not support UDS file types
|
||||
* (e.g. smbfs to a Windows server) or if the remote kernel does not
|
||||
* allow a non-local process to bind() the socket. (These problems
|
||||
* could be fixed by moving the UDS out of the .git directory and to a
|
||||
* well-known local directory on the client machine, but care should
|
||||
* be taken to ensure that $HOME is actually local and not a managed
|
||||
* file share.)
|
||||
*
|
||||
* So (for now at least), mark remote working directories as
|
||||
* incompatible.
|
||||
*/
|
||||
static enum fsmonitor_reason is_remote(struct repository *r)
|
||||
{
|
||||
struct statfs fs;
|
||||
|
||||
if (statfs(r->worktree, &fs) == -1) {
|
||||
int saved_errno = errno;
|
||||
trace_printf_key(&trace_fsmonitor, "statfs('%s') failed: %s",
|
||||
r->worktree, strerror(saved_errno));
|
||||
errno = saved_errno;
|
||||
return FSMONITOR_REASON_ZERO;
|
||||
}
|
||||
|
||||
trace_printf_key(&trace_fsmonitor,
|
||||
"statfs('%s') [type 0x%08x][flags 0x%08x] '%s'",
|
||||
r->worktree, fs.f_type, fs.f_flags, fs.f_fstypename);
|
||||
|
||||
if (!(fs.f_flags & MNT_LOCAL))
|
||||
return FSMONITOR_REASON_REMOTE;
|
||||
|
||||
return FSMONITOR_REASON_ZERO;
|
||||
}
|
||||
|
||||
enum fsmonitor_reason fsm_os__incompatible(struct repository *r)
|
||||
{
|
||||
enum fsmonitor_reason reason;
|
||||
|
||||
reason = is_remote(r);
|
||||
if (reason)
|
||||
return reason;
|
||||
|
||||
return FSMONITOR_REASON_ZERO;
|
||||
}
|
||||
137
compat/fsmonitor/fsm-settings-win32.c
Normal file
137
compat/fsmonitor/fsm-settings-win32.c
Normal file
@@ -0,0 +1,137 @@
|
||||
#include "cache.h"
|
||||
#include "config.h"
|
||||
#include "repository.h"
|
||||
#include "fsmonitor-settings.h"
|
||||
#include "fsmonitor.h"
|
||||
|
||||
/*
|
||||
* GVFS (aka VFS for Git) is incompatible with FSMonitor.
|
||||
*
|
||||
* Granted, core Git does not know anything about GVFS and we
|
||||
* shouldn't make assumptions about a downstream feature, but users
|
||||
* can install both versions. And this can lead to incorrect results
|
||||
* from core Git commands. So, without bringing in any of the GVFS
|
||||
* code, do a simple config test for a published config setting. (We
|
||||
* do not look at the various *_TEST_* environment variables.)
|
||||
*/
|
||||
static enum fsmonitor_reason is_virtual(struct repository *r)
|
||||
{
|
||||
const char *const_str;
|
||||
|
||||
if (!repo_config_get_value(r, "core.virtualfilesystem", &const_str))
|
||||
return FSMONITOR_REASON_VIRTUAL;
|
||||
|
||||
return FSMONITOR_REASON_ZERO;
|
||||
}
|
||||
|
||||
/*
|
||||
* Remote working directories are problematic for FSMonitor.
|
||||
*
|
||||
* The underlying file system on the server machine and/or the remote
|
||||
* mount type dictates whether notification events are available at
|
||||
* all to remote client machines.
|
||||
*
|
||||
* Kernel differences between the server and client machines also
|
||||
* dictate the how (buffering, frequency, de-dup) the events are
|
||||
* delivered to client machine processes.
|
||||
*
|
||||
* A client machine (such as a laptop) may choose to suspend/resume
|
||||
* and it is unclear (without lots of testing) whether the watcher can
|
||||
* resync after a resume. We might be able to treat this as a normal
|
||||
* "events were dropped by the kernel" event and do our normal "flush
|
||||
* and resync" --or-- we might need to close the existing (zombie?)
|
||||
* notification fd and create a new one.
|
||||
*
|
||||
* In theory, the above issues need to be addressed whether we are
|
||||
* using the Hook or IPC API.
|
||||
*
|
||||
* So (for now at least), mark remote working directories as
|
||||
* incompatible.
|
||||
*
|
||||
* Notes for testing:
|
||||
*
|
||||
* (a) Windows allows a network share to be mapped to a drive letter.
|
||||
* (This is the normal method to access it.)
|
||||
*
|
||||
* $ NET USE Z: \\server\share
|
||||
* $ git -C Z:/repo status
|
||||
*
|
||||
* (b) Windows allows a network share to be referenced WITHOUT mapping
|
||||
* it to drive letter.
|
||||
*
|
||||
* $ NET USE \\server\share\dir
|
||||
* $ git -C //server/share/repo status
|
||||
*
|
||||
* (c) Windows allows "SUBST" to create a fake drive mapping to an
|
||||
* arbitrary path (which may be remote)
|
||||
*
|
||||
* $ SUBST Q: Z:\repo
|
||||
* $ git -C Q:/ status
|
||||
*
|
||||
* (d) Windows allows a directory symlink to be created on a local
|
||||
* file system that points to a remote repo.
|
||||
*
|
||||
* $ mklink /d ./link //server/share/repo
|
||||
* $ git -C ./link status
|
||||
*/
|
||||
static enum fsmonitor_reason is_remote(struct repository *r)
|
||||
{
|
||||
wchar_t wpath[MAX_PATH];
|
||||
wchar_t wfullpath[MAX_PATH];
|
||||
size_t wlen;
|
||||
UINT driveType;
|
||||
|
||||
/*
|
||||
* Do everything in wide chars because the drive letter might be
|
||||
* a multi-byte sequence. See win32_has_dos_drive_prefix().
|
||||
*/
|
||||
if (xutftowcs_path(wpath, r->worktree) < 0)
|
||||
return FSMONITOR_REASON_ZERO;
|
||||
|
||||
/*
|
||||
* GetDriveTypeW() requires a final slash. We assume that the
|
||||
* worktree pathname points to an actual directory.
|
||||
*/
|
||||
wlen = wcslen(wpath);
|
||||
if (wpath[wlen - 1] != L'\\' && wpath[wlen - 1] != L'/') {
|
||||
wpath[wlen++] = L'\\';
|
||||
wpath[wlen] = 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Normalize the path. If nothing else, this converts forward
|
||||
* slashes to backslashes. This is essential to get GetDriveTypeW()
|
||||
* correctly handle some UNC "\\server\share\..." paths.
|
||||
*/
|
||||
if (!GetFullPathNameW(wpath, MAX_PATH, wfullpath, NULL))
|
||||
return FSMONITOR_REASON_ZERO;
|
||||
|
||||
driveType = GetDriveTypeW(wfullpath);
|
||||
trace_printf_key(&trace_fsmonitor,
|
||||
"DriveType '%s' L'%ls' (%u)",
|
||||
r->worktree, wfullpath, driveType);
|
||||
|
||||
if (driveType == DRIVE_REMOTE) {
|
||||
trace_printf_key(&trace_fsmonitor,
|
||||
"is_remote('%s') true",
|
||||
r->worktree);
|
||||
return FSMONITOR_REASON_REMOTE;
|
||||
}
|
||||
|
||||
return FSMONITOR_REASON_ZERO;
|
||||
}
|
||||
|
||||
enum fsmonitor_reason fsm_os__incompatible(struct repository *r)
|
||||
{
|
||||
enum fsmonitor_reason reason;
|
||||
|
||||
reason = is_virtual(r);
|
||||
if (reason)
|
||||
return reason;
|
||||
|
||||
reason = is_remote(r);
|
||||
if (reason)
|
||||
return reason;
|
||||
|
||||
return FSMONITOR_REASON_ZERO;
|
||||
}
|
||||
@@ -163,6 +163,7 @@ ifeq ($(uname_S),Darwin)
|
||||
ifndef NO_PTHREADS
|
||||
ifndef NO_UNIX_SOCKETS
|
||||
FSMONITOR_DAEMON_BACKEND = darwin
|
||||
FSMONITOR_OS_SETTINGS = darwin
|
||||
endif
|
||||
endif
|
||||
|
||||
@@ -449,6 +450,8 @@ ifeq ($(uname_S),Windows)
|
||||
# These are always available, so we do not have to conditionally
|
||||
# support it.
|
||||
FSMONITOR_DAEMON_BACKEND = win32
|
||||
FSMONITOR_OS_SETTINGS = win32
|
||||
|
||||
NO_SVN_TESTS = YesPlease
|
||||
RUNTIME_PREFIX = YesPlease
|
||||
HAVE_WPGMPTR = YesWeDo
|
||||
@@ -643,6 +646,8 @@ ifeq ($(uname_S),MINGW)
|
||||
# These are always available, so we do not have to conditionally
|
||||
# support it.
|
||||
FSMONITOR_DAEMON_BACKEND = win32
|
||||
FSMONITOR_OS_SETTINGS = win32
|
||||
|
||||
RUNTIME_PREFIX = YesPlease
|
||||
HAVE_WPGMPTR = YesWeDo
|
||||
NO_ST_BLOCKS_IN_STRUCT_STAT = YesPlease
|
||||
|
||||
@@ -314,9 +314,17 @@ if(SUPPORTS_SIMPLE_IPC)
|
||||
if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
|
||||
add_compile_definitions(HAVE_FSMONITOR_DAEMON_BACKEND)
|
||||
list(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-win32.c)
|
||||
list(APPEND compat_SOURCES compat/fsmonitor/fsm-health-win32.c)
|
||||
|
||||
add_compile_definitions(HAVE_FSMONITOR_OS_SETTINGS)
|
||||
list(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-win32.c)
|
||||
elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
|
||||
add_compile_definitions(HAVE_FSMONITOR_DAEMON_BACKEND)
|
||||
list(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-darwin.c)
|
||||
list(APPEND compat_SOURCES compat/fsmonitor/fsm-health-darwin.c)
|
||||
|
||||
add_compile_definitions(HAVE_FSMONITOR_OS_SETTINGS)
|
||||
list(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-darwin.c)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
|
||||
@@ -33,10 +33,12 @@ void fsmonitor_batch__free_list(struct fsmonitor_batch *batch);
|
||||
*/
|
||||
void fsmonitor_batch__add_path(struct fsmonitor_batch *batch, const char *path);
|
||||
|
||||
struct fsmonitor_daemon_backend_data; /* opaque platform-specific data */
|
||||
struct fsm_listen_data; /* opaque platform-specific data for listener thread */
|
||||
struct fsm_health_data; /* opaque platform-specific data for health thread */
|
||||
|
||||
struct fsmonitor_daemon_state {
|
||||
pthread_t listener_thread;
|
||||
pthread_t health_thread;
|
||||
pthread_mutex_t main_lock;
|
||||
|
||||
struct strbuf path_worktree_watch;
|
||||
@@ -50,10 +52,13 @@ struct fsmonitor_daemon_state {
|
||||
int cookie_seq;
|
||||
struct hashmap cookies;
|
||||
|
||||
int error_code;
|
||||
struct fsmonitor_daemon_backend_data *backend_data;
|
||||
int listen_error_code;
|
||||
int health_error_code;
|
||||
struct fsm_listen_data *listen_data;
|
||||
struct fsm_health_data *health_data;
|
||||
|
||||
struct ipc_server_data *ipc_server_data;
|
||||
struct strbuf path_ipc;
|
||||
};
|
||||
|
||||
/*
|
||||
|
||||
@@ -9,19 +9,69 @@
|
||||
*/
|
||||
struct fsmonitor_settings {
|
||||
enum fsmonitor_mode mode;
|
||||
enum fsmonitor_reason reason;
|
||||
char *hook_path;
|
||||
};
|
||||
|
||||
void fsm_settings__set_ipc(struct repository *r)
|
||||
static void set_incompatible(struct repository *r,
|
||||
enum fsmonitor_reason reason)
|
||||
{
|
||||
struct fsmonitor_settings *s = r->settings.fsmonitor;
|
||||
|
||||
s->mode = FSMONITOR_MODE_INCOMPATIBLE;
|
||||
s->reason = reason;
|
||||
}
|
||||
|
||||
static int check_for_incompatible(struct repository *r)
|
||||
{
|
||||
if (!r->worktree) {
|
||||
/*
|
||||
* Bare repositories don't have a working directory and
|
||||
* therefore have nothing to watch.
|
||||
*/
|
||||
set_incompatible(r, FSMONITOR_REASON_BARE);
|
||||
return 1;
|
||||
}
|
||||
|
||||
#ifdef HAVE_FSMONITOR_OS_SETTINGS
|
||||
{
|
||||
enum fsmonitor_reason reason;
|
||||
|
||||
reason = fsm_os__incompatible(r);
|
||||
if (reason != FSMONITOR_REASON_ZERO) {
|
||||
set_incompatible(r, reason);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static struct fsmonitor_settings *s_init(struct repository *r)
|
||||
{
|
||||
if (!r->settings.fsmonitor)
|
||||
CALLOC_ARRAY(r->settings.fsmonitor, 1);
|
||||
|
||||
return r->settings.fsmonitor;
|
||||
}
|
||||
|
||||
void fsm_settings__set_ipc(struct repository *r)
|
||||
{
|
||||
struct fsmonitor_settings *s = s_init(r);
|
||||
|
||||
if (check_for_incompatible(r))
|
||||
return;
|
||||
|
||||
s->mode = FSMONITOR_MODE_IPC;
|
||||
}
|
||||
|
||||
void fsm_settings__set_hook(struct repository *r, const char *path)
|
||||
{
|
||||
struct fsmonitor_settings *s = r->settings.fsmonitor;
|
||||
struct fsmonitor_settings *s = s_init(r);
|
||||
|
||||
if (check_for_incompatible(r))
|
||||
return;
|
||||
|
||||
s->mode = FSMONITOR_MODE_HOOK;
|
||||
s->hook_path = strdup(path);
|
||||
@@ -29,9 +79,10 @@ void fsm_settings__set_hook(struct repository *r, const char *path)
|
||||
|
||||
void fsm_settings__set_disabled(struct repository *r)
|
||||
{
|
||||
struct fsmonitor_settings *s = r->settings.fsmonitor;
|
||||
struct fsmonitor_settings *s = s_init(r);
|
||||
|
||||
s->mode = FSMONITOR_MODE_DISABLED;
|
||||
s->reason = FSMONITOR_REASON_ZERO;
|
||||
FREE_AND_NULL(s->hook_path);
|
||||
}
|
||||
|
||||
@@ -65,12 +116,6 @@ static int check_for_hook(struct repository *r)
|
||||
|
||||
static void lookup_fsmonitor_settings(struct repository *r)
|
||||
{
|
||||
struct fsmonitor_settings *s;
|
||||
|
||||
CALLOC_ARRAY(s, 1);
|
||||
|
||||
r->settings.fsmonitor = s;
|
||||
|
||||
if (check_for_ipc(r))
|
||||
return;
|
||||
|
||||
@@ -95,3 +140,45 @@ const char *fsm_settings__get_hook_path(struct repository *r)
|
||||
|
||||
return r->settings.fsmonitor->hook_path;
|
||||
}
|
||||
|
||||
static void create_reason_message(struct repository *r,
|
||||
struct strbuf *buf_reason)
|
||||
{
|
||||
struct fsmonitor_settings *s = r->settings.fsmonitor;
|
||||
|
||||
switch (s->reason) {
|
||||
case FSMONITOR_REASON_ZERO:
|
||||
return;
|
||||
|
||||
case FSMONITOR_REASON_BARE:
|
||||
strbuf_addstr(buf_reason,
|
||||
_("bare repos are incompatible with fsmonitor"));
|
||||
return;
|
||||
|
||||
case FSMONITOR_REASON_VIRTUAL:
|
||||
strbuf_addstr(buf_reason,
|
||||
_("virtual repos are incompatible with fsmonitor"));
|
||||
return;
|
||||
|
||||
case FSMONITOR_REASON_REMOTE:
|
||||
strbuf_addstr(buf_reason,
|
||||
_("remote repos are incompatible with fsmonitor"));
|
||||
return;
|
||||
|
||||
default:
|
||||
BUG("Unhandled case in create_reason_message '%d'", s->reason);
|
||||
}
|
||||
}
|
||||
enum fsmonitor_reason fsm_settings__get_reason(struct repository *r,
|
||||
struct strbuf *buf_reason)
|
||||
{
|
||||
strbuf_reset(buf_reason);
|
||||
|
||||
if (!r->settings.fsmonitor)
|
||||
lookup_fsmonitor_settings(r);
|
||||
|
||||
if (r->settings.fsmonitor->mode == FSMONITOR_MODE_INCOMPATIBLE)
|
||||
create_reason_message(r, buf_reason);
|
||||
|
||||
return r->settings.fsmonitor->reason;
|
||||
}
|
||||
|
||||
@@ -4,18 +4,44 @@
|
||||
struct repository;
|
||||
|
||||
enum fsmonitor_mode {
|
||||
FSMONITOR_MODE_INCOMPATIBLE = -1, /* see _reason */
|
||||
FSMONITOR_MODE_DISABLED = 0,
|
||||
FSMONITOR_MODE_HOOK = 1, /* core.fsmonitor */
|
||||
FSMONITOR_MODE_IPC = 2, /* core.useBuiltinFSMonitor */
|
||||
};
|
||||
|
||||
/*
|
||||
* Incompatibility reasons.
|
||||
*/
|
||||
enum fsmonitor_reason {
|
||||
FSMONITOR_REASON_ZERO = 0,
|
||||
FSMONITOR_REASON_BARE = 1,
|
||||
FSMONITOR_REASON_VIRTUAL = 2,
|
||||
FSMONITOR_REASON_REMOTE = 3,
|
||||
};
|
||||
|
||||
void fsm_settings__set_ipc(struct repository *r);
|
||||
void fsm_settings__set_hook(struct repository *r, const char *path);
|
||||
void fsm_settings__set_disabled(struct repository *r);
|
||||
|
||||
enum fsmonitor_mode fsm_settings__get_mode(struct repository *r);
|
||||
const char *fsm_settings__get_hook_path(struct repository *r);
|
||||
enum fsmonitor_reason fsm_settings__get_reason(struct repository *r,
|
||||
struct strbuf *buf_reason);
|
||||
|
||||
struct fsmonitor_settings;
|
||||
|
||||
#ifdef HAVE_FSMONITOR_OS_SETTINGS
|
||||
/*
|
||||
* Ask platform-specific code whether the repository is incompatible
|
||||
* with fsmonitor (both hook and ipc modes). For example, if the working
|
||||
* directory is on a remote volume and mounted via a technology that does
|
||||
* not support notification events.
|
||||
*
|
||||
* fsm_os__* routines should considered private to fsm_settings__
|
||||
* routines.
|
||||
*/
|
||||
enum fsmonitor_reason fsm_os__incompatible(struct repository *r);
|
||||
#endif /* HAVE_FSMONITOR_OS_SETTINGS */
|
||||
|
||||
#endif /* FSMONITOR_SETTINGS_H */
|
||||
|
||||
80
fsmonitor.c
80
fsmonitor.c
@@ -198,30 +198,68 @@ int fsmonitor_is_trivial_response(const struct strbuf *query_result)
|
||||
static void fsmonitor_refresh_callback(struct index_state *istate, char *name)
|
||||
{
|
||||
int i, len = strlen(name);
|
||||
if (name[len - 1] == '/') {
|
||||
int pos = index_name_pos(istate, name, len);
|
||||
|
||||
trace_printf_key(&trace_fsmonitor,
|
||||
"fsmonitor_refresh_callback '%s' (pos %d)",
|
||||
name, pos);
|
||||
|
||||
if (name[len - 1] == '/') {
|
||||
/*
|
||||
* TODO We should binary search to find the first path with
|
||||
* TODO this directory prefix. Then linearly update entries
|
||||
* TODO while the prefix matches. Taking care to search without
|
||||
* TODO the trailing slash -- because '/' sorts after a few
|
||||
* TODO interesting special chars, like '.' and ' '.
|
||||
* The daemon can decorate directory events, such as
|
||||
* moves or renames, with a trailing slash if the OS
|
||||
* FS Event contains sufficient information, such as
|
||||
* MacOS.
|
||||
*
|
||||
* Use this to invalidate the entire cone under that
|
||||
* directory.
|
||||
*
|
||||
* We do not expect an exact match because the index
|
||||
* does not normally contain directory entries, so we
|
||||
* start at the insertion point and scan.
|
||||
*/
|
||||
if (pos < 0)
|
||||
pos = -pos - 1;
|
||||
|
||||
/* Mark all entries for the folder invalid */
|
||||
for (i = 0; i < istate->cache_nr; i++) {
|
||||
if (istate->cache[i]->ce_flags & CE_FSMONITOR_VALID &&
|
||||
starts_with(istate->cache[i]->name, name))
|
||||
istate->cache[i]->ce_flags &= ~CE_FSMONITOR_VALID;
|
||||
for (i = pos; i < istate->cache_nr; i++) {
|
||||
if (!starts_with(istate->cache[i]->name, name))
|
||||
break;
|
||||
istate->cache[i]->ce_flags &= ~CE_FSMONITOR_VALID;
|
||||
}
|
||||
/* Need to remove the / from the path for the untracked cache */
|
||||
name[len - 1] = '\0';
|
||||
} else {
|
||||
int pos = index_name_pos(istate, name, strlen(name));
|
||||
|
||||
if (pos >= 0) {
|
||||
struct cache_entry *ce = istate->cache[pos];
|
||||
ce->ce_flags &= ~CE_FSMONITOR_VALID;
|
||||
/*
|
||||
* We need to remove the traling "/" from the path
|
||||
* for the untracked cache.
|
||||
*/
|
||||
name[len - 1] = '\0';
|
||||
} else if (pos >= 0) {
|
||||
/*
|
||||
* We have an exact match for this path and can just
|
||||
* invalidate it.
|
||||
*/
|
||||
istate->cache[pos]->ce_flags &= ~CE_FSMONITOR_VALID;
|
||||
} else {
|
||||
/*
|
||||
* The path is not a tracked file -or- it is a
|
||||
* directory event on a platform that cannot
|
||||
* distinguish between file and directory events in
|
||||
* the event handler, such as Windows.
|
||||
*
|
||||
* Scan as if it is a directory and invalidate the
|
||||
* cone under it. (But remember to ignore items
|
||||
* between "name" and "name/", such as "name-" and
|
||||
* "name.".
|
||||
*/
|
||||
pos = -pos - 1;
|
||||
|
||||
for (i = pos; i < istate->cache_nr; i++) {
|
||||
if (!starts_with(istate->cache[i]->name, name))
|
||||
break;
|
||||
if ((unsigned char)istate->cache[i]->name[len] > '/')
|
||||
break;
|
||||
if (istate->cache[i]->name[len] == '/')
|
||||
istate->cache[i]->ce_flags &= ~CE_FSMONITOR_VALID;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,7 +267,6 @@ static void fsmonitor_refresh_callback(struct index_state *istate, char *name)
|
||||
* Mark the untracked cache dirty even if it wasn't found in the index
|
||||
* as it could be a new untracked file.
|
||||
*/
|
||||
trace_printf_key(&trace_fsmonitor, "fsmonitor_refresh_callback '%s'", name);
|
||||
untracked_cache_invalidate_path(istate, name, 0);
|
||||
}
|
||||
|
||||
@@ -403,6 +440,8 @@ apply_results:
|
||||
* information and that we should consider everything
|
||||
* invalid. We call this a trivial response.
|
||||
*/
|
||||
trace2_region_enter("fsmonitor", "apply_results", istate->repo);
|
||||
|
||||
if (query_success && query_result.buf[bol] != '/') {
|
||||
/*
|
||||
* Mark all pathnames returned by the monitor as dirty.
|
||||
@@ -431,6 +470,9 @@ apply_results:
|
||||
if (count > fsmonitor_force_update_threshold)
|
||||
istate->cache_changed |= FSMONITOR_CHANGED;
|
||||
|
||||
trace2_data_intmax("fsmonitor", istate->repo, "apply_count",
|
||||
count);
|
||||
|
||||
} else {
|
||||
/*
|
||||
* We received a trivial response, so invalidate everything.
|
||||
@@ -458,6 +500,8 @@ apply_results:
|
||||
if (istate->untracked)
|
||||
istate->untracked->use_fsmonitor = 0;
|
||||
}
|
||||
trace2_region_leave("fsmonitor", "apply_results", istate->repo);
|
||||
|
||||
strbuf_release(&query_result);
|
||||
|
||||
/* Now that we've updated istate, save the last_update_token */
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
#include "cache.h"
|
||||
#include "parse-options.h"
|
||||
#include "fsmonitor-ipc.h"
|
||||
#include "thread-utils.h"
|
||||
#include "trace2.h"
|
||||
|
||||
#ifndef HAVE_FSMONITOR_DAEMON_BACKEND
|
||||
int cmd__fsmonitor_client(int argc, const char **argv)
|
||||
@@ -79,20 +81,120 @@ static int do_send_flush(void)
|
||||
return 0;
|
||||
}
|
||||
|
||||
struct hammer_thread_data
|
||||
{
|
||||
pthread_t pthread_id;
|
||||
int thread_nr;
|
||||
|
||||
int nr_requests;
|
||||
const char *token;
|
||||
|
||||
int sum_successful;
|
||||
int sum_errors;
|
||||
};
|
||||
|
||||
static void *hammer_thread_proc(void *_hammer_thread_data)
|
||||
{
|
||||
struct hammer_thread_data *data = _hammer_thread_data;
|
||||
struct strbuf answer = STRBUF_INIT;
|
||||
int k;
|
||||
int ret;
|
||||
|
||||
trace2_thread_start("hammer");
|
||||
|
||||
for (k = 0; k < data->nr_requests; k++) {
|
||||
strbuf_reset(&answer);
|
||||
|
||||
ret = fsmonitor_ipc__send_query(data->token, &answer);
|
||||
if (ret < 0)
|
||||
data->sum_errors++;
|
||||
else
|
||||
data->sum_successful++;
|
||||
}
|
||||
|
||||
strbuf_release(&answer);
|
||||
trace2_thread_exit();
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/*
|
||||
* Start a pool of client threads that will each send a series of
|
||||
* commands to the daemon.
|
||||
*
|
||||
* The goal is to overload the daemon with a sustained series of
|
||||
* concurrent requests.
|
||||
*/
|
||||
static int do_hammer(const char *token, int nr_threads, int nr_requests)
|
||||
{
|
||||
struct hammer_thread_data *data = NULL;
|
||||
int k;
|
||||
int sum_join_errors = 0;
|
||||
int sum_commands = 0;
|
||||
int sum_errors = 0;
|
||||
|
||||
if (!token || !*token)
|
||||
token = get_token_from_index();
|
||||
if (nr_threads < 1)
|
||||
nr_threads = 1;
|
||||
if (nr_requests < 1)
|
||||
nr_requests = 1;
|
||||
|
||||
CALLOC_ARRAY(data, nr_threads);
|
||||
|
||||
for (k = 0; k < nr_threads; k++) {
|
||||
struct hammer_thread_data *p = &data[k];
|
||||
p->thread_nr = k;
|
||||
p->nr_requests = nr_requests;
|
||||
p->token = token;
|
||||
|
||||
if (pthread_create(&p->pthread_id, NULL, hammer_thread_proc, p)) {
|
||||
warning("failed to create thread[%d] skipping remainder", k);
|
||||
nr_threads = k;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (k = 0; k < nr_threads; k++) {
|
||||
struct hammer_thread_data *p = &data[k];
|
||||
|
||||
if (pthread_join(p->pthread_id, NULL))
|
||||
sum_join_errors++;
|
||||
sum_commands += p->sum_successful;
|
||||
sum_errors += p->sum_errors;
|
||||
}
|
||||
|
||||
fprintf(stderr, "HAMMER: [threads %d][requests %d] [ok %d][err %d][join %d]\n",
|
||||
nr_threads, nr_requests, sum_commands, sum_errors, sum_join_errors);
|
||||
|
||||
free(data);
|
||||
|
||||
/*
|
||||
* TODO Decide if/when to return an error or call die().
|
||||
*/
|
||||
return 0;
|
||||
}
|
||||
|
||||
int cmd__fsmonitor_client(int argc, const char **argv)
|
||||
{
|
||||
const char *subcmd;
|
||||
const char *token = NULL;
|
||||
int nr_threads = 1;
|
||||
int nr_requests = 1;
|
||||
|
||||
const char * const fsmonitor_client_usage[] = {
|
||||
N_("test-helper fsmonitor-client query [<token>]"),
|
||||
N_("test-helper fsmonitor-client flush"),
|
||||
N_("test-helper fsmonitor-client hammer [<token>] [<threads>] [<requests>]"),
|
||||
NULL,
|
||||
};
|
||||
|
||||
struct option options[] = {
|
||||
OPT_STRING(0, "token", &token, N_("token"),
|
||||
N_("command token to send to the server")),
|
||||
|
||||
OPT_INTEGER(0, "threads", &nr_threads, N_("number of client threads")),
|
||||
OPT_INTEGER(0, "requests", &nr_requests, N_("number of requests per thread")),
|
||||
|
||||
OPT_END()
|
||||
};
|
||||
|
||||
@@ -116,6 +218,9 @@ int cmd__fsmonitor_client(int argc, const char **argv)
|
||||
if (!strcmp(subcmd, "flush"))
|
||||
return !!do_send_flush();
|
||||
|
||||
if (!strcmp(subcmd, "hammer"))
|
||||
return !!do_hammer(token, nr_threads, nr_requests);
|
||||
|
||||
die("Unhandled subcommand: '%s'", subcmd);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -55,6 +55,41 @@ test_lazy_prereq UNTRACKED_CACHE '
|
||||
test $ret -ne 1
|
||||
'
|
||||
|
||||
# Test that we detect and disallow repos that are incompatible with FSMonitor.
|
||||
test_expect_success 'incompatible bare repo' '
|
||||
test_when_finished "rm -rf ./bare-clone actual expect" &&
|
||||
git init --bare bare-clone &&
|
||||
cat >expect <<-\EOF &&
|
||||
error: bare repos are incompatible with fsmonitor
|
||||
EOF
|
||||
|
||||
test_must_fail \
|
||||
git -C ./bare-clone -c core.fsmonitor=foo \
|
||||
update-index --fsmonitor 2>actual &&
|
||||
test_cmp expect actual &&
|
||||
|
||||
test_must_fail \
|
||||
git -C ./bare-clone -c core.usebuiltinfsmonitor=true \
|
||||
update-index --fsmonitor 2>actual &&
|
||||
test_cmp expect actual
|
||||
'
|
||||
|
||||
test_expect_success FSMONITOR_DAEMON 'run fsmonitor-daemon in bare repo' '
|
||||
test_when_finished "rm -rf ./bare-clone actual" &&
|
||||
git init --bare bare-clone &&
|
||||
test_must_fail git -C ./bare-clone fsmonitor--daemon run 2>actual &&
|
||||
grep "bare repos are incompatible with fsmonitor" actual
|
||||
'
|
||||
|
||||
test_expect_success MINGW,FSMONITOR_DAEMON 'run fsmonitor-daemon in virtual repo' '
|
||||
test_when_finished "rm -rf ./fake-virtual-clone actual" &&
|
||||
git init fake-virtual-clone &&
|
||||
test_must_fail git -C ./fake-virtual-clone \
|
||||
-c core.virtualfilesystem=true \
|
||||
fsmonitor--daemon run 2>actual &&
|
||||
grep "virtual repos are incompatible with fsmonitor" actual
|
||||
'
|
||||
|
||||
test_expect_success 'setup' '
|
||||
mkdir -p .git/hooks &&
|
||||
: >tracked &&
|
||||
|
||||
@@ -123,6 +123,101 @@ test_expect_success 'implicit daemon stop (rename .git)' '
|
||||
test_must_fail git -C test_implicit_2 fsmonitor--daemon status
|
||||
'
|
||||
|
||||
# File systems on Windows may or may not have shortnames.
|
||||
# This is a volume-specific setting on modern systems.
|
||||
# "C:/" drives are required to have them enabled. Other
|
||||
# hard drives default to disabled.
|
||||
#
|
||||
# This is a crude test to see if shortnames are enabled
|
||||
# on the volume containing the test directory. It is
|
||||
# crude, but it does not require elevation like `fsutil`.
|
||||
#
|
||||
test_lazy_prereq SHORTNAMES '
|
||||
mkdir .foo &&
|
||||
test -d "FOO~1"
|
||||
'
|
||||
|
||||
# Here we assume that the shortname of ".git" is "GIT~1".
|
||||
test_expect_success MINGW,SHORTNAMES 'implicit daemon stop (rename GIT~1)' '
|
||||
test_when_finished "stop_daemon_delete_repo test_implicit_1s" &&
|
||||
|
||||
git init test_implicit_1s &&
|
||||
|
||||
start_daemon test_implicit_1s &&
|
||||
|
||||
# renaming the .git directory will implicitly stop the daemon.
|
||||
# this moves {.git, GIT~1} to {.gitxyz, GITXYZ~1}.
|
||||
# the rename-from FS Event will contain the shortname.
|
||||
#
|
||||
mv test_implicit_1s/GIT~1 test_implicit_1s/.gitxyz &&
|
||||
|
||||
sleep 1 &&
|
||||
# put it back so that our status will not crawl out to our
|
||||
# parent directory.
|
||||
# this moves {.gitxyz, GITXYZ~1} to {.git, GIT~1}.
|
||||
mv test_implicit_1s/.gitxyz test_implicit_1s/.git &&
|
||||
|
||||
test_must_fail git -C test_implicit_1s fsmonitor--daemon status
|
||||
'
|
||||
|
||||
# Here we first create a file with LONGNAME of "GIT~1" before
|
||||
# we create the repo. This will cause the shortname of ".git"
|
||||
# to be "GIT~2".
|
||||
test_expect_success MINGW,SHORTNAMES 'implicit daemon stop (rename GIT~2)' '
|
||||
test_when_finished "stop_daemon_delete_repo test_implicit_1s2" &&
|
||||
|
||||
mkdir test_implicit_1s2 &&
|
||||
echo HELLO >test_implicit_1s2/GIT~1 &&
|
||||
git init test_implicit_1s2 &&
|
||||
|
||||
test_path_is_file test_implicit_1s2/GIT~1 &&
|
||||
test_path_is_dir test_implicit_1s2/GIT~2 &&
|
||||
|
||||
start_daemon test_implicit_1s2 &&
|
||||
|
||||
# renaming the .git directory will implicitly stop the daemon.
|
||||
# the rename-from FS Event will contain the shortname.
|
||||
#
|
||||
mv test_implicit_1s2/GIT~2 test_implicit_1s2/.gitxyz &&
|
||||
|
||||
sleep 1 &&
|
||||
# put it back so that our status will not crawl out to our
|
||||
# parent directory.
|
||||
mv test_implicit_1s2/.gitxyz test_implicit_1s2/.git &&
|
||||
|
||||
test_must_fail git -C test_implicit_1s2 fsmonitor--daemon status
|
||||
'
|
||||
|
||||
# Confirm that MacOS hides all of the Unicode normalization and/or
|
||||
# case folding from the FS events. That is, are the pathnames in the
|
||||
# FS events reported using the spelling on the disk or in the spelling
|
||||
# used by the other process.
|
||||
#
|
||||
# Note that we assume that the filesystem is set to case insensitive.
|
||||
#
|
||||
# NEEDSWORK: APFS handles Unicode and Unicode normalization
|
||||
# differently than HFS+. I only have an APFS partition, so
|
||||
# more testing here would be helpful.
|
||||
#
|
||||
|
||||
# Rename .git using alternate spelling and confirm that the daemon
|
||||
# sees the event using the correct spelling and shutdown.
|
||||
test_expect_success UTF8_NFD_TO_NFC 'MacOS event spelling (rename .GIT)' '
|
||||
test_when_finished "stop_daemon_delete_repo test_apfs" &&
|
||||
|
||||
git init test_apfs &&
|
||||
start_daemon test_apfs &&
|
||||
|
||||
test_path_is_dir test_apfs/.git &&
|
||||
test_path_is_dir test_apfs/.GIT &&
|
||||
|
||||
mv test_apfs/.GIT test_apfs/.FOO &&
|
||||
sleep 1 &&
|
||||
mv test_apfs/.FOO test_apfs/.git &&
|
||||
|
||||
test_must_fail git -C test_apfs fsmonitor--daemon status
|
||||
'
|
||||
|
||||
test_expect_success 'cannot start multiple daemons' '
|
||||
test_when_finished "stop_daemon_delete_repo test_multiple" &&
|
||||
|
||||
@@ -604,4 +699,27 @@ do
|
||||
done
|
||||
done
|
||||
|
||||
# Test Unicode UTF-8 characters in the pathname of the working
|
||||
# directory. Use of "*A()" routines rather than "*W()" routines
|
||||
# on Windows can sometimes lead to odd failures.
|
||||
#
|
||||
u1=$(printf "u_c3_a6__\xC3\xA6")
|
||||
u2=$(printf "u_e2_99_ab__\xE2\x99\xAB")
|
||||
u_values="$u1 $u2"
|
||||
for u in $u_values
|
||||
do
|
||||
test_expect_success "Unicode path: $u" '
|
||||
test_when_finished "stop_daemon_delete_repo $u" &&
|
||||
|
||||
git init "$u" &&
|
||||
echo 1 >"$u"/file1 &&
|
||||
git -C "$u" add file1 &&
|
||||
git -C "$u" config core.useBuiltinFSMonitor true &&
|
||||
|
||||
start_daemon "$u" &&
|
||||
git -C "$u" status >actual &&
|
||||
grep "new file: file1" actual
|
||||
'
|
||||
done
|
||||
|
||||
test_done
|
||||
|
||||
@@ -1772,6 +1772,7 @@ int unpack_trees(unsigned len, struct tree_desc *t, struct unpack_trees_options
|
||||
|
||||
o->result.fsmonitor_last_update =
|
||||
xstrdup_or_null(o->src_index->fsmonitor_last_update);
|
||||
o->result.fsmonitor_has_run_once = o->src_index->fsmonitor_has_run_once;
|
||||
|
||||
/*
|
||||
* Sparse checkout loop #1: set NEW_SKIP_WORKTREE on existing entries
|
||||
|
||||
Reference in New Issue
Block a user