mirror of
https://github.com/git/git.git
synced 2026-03-04 14:37:35 +01:00
Merge branch 'pt/fsmonitor-linux' into seen
The fsmonitor daemon has been implemented for Linux. * pt/fsmonitor-linux: fsmonitor: close inherited file descriptors and detach in daemon run-command: add close_fd_above_stderr option fsmonitor: add tests for Linux fsmonitor: implement filesystem change listener for Linux fsmonitor: deduplicate settings logic for Unix platforms fsmonitor: deduplicate IPC path logic for Unix platforms fsmonitor: use pthread_cond_timedwait for cookie wait compat/win32: add pthread_cond_timedwait fsmonitor: fix hashmap memory leak in fsmonitor_run_daemon fsmonitor: fix khash memory leak in do_handle_client
This commit is contained in:
@@ -4,8 +4,8 @@ fsmonitor.allowRemote::
|
||||
behavior. Only respected when `core.fsmonitor` is set to `true`.
|
||||
|
||||
fsmonitor.socketDir::
|
||||
This Mac OS-specific option, if set, specifies the directory in
|
||||
This Mac OS and Linux-specific option, if set, specifies the directory in
|
||||
which to create the Unix domain socket used for communication
|
||||
between the fsmonitor daemon and various Git commands. The directory must
|
||||
reside on a native Mac OS filesystem. Only respected when `core.fsmonitor`
|
||||
reside on a native filesystem. Only respected when `core.fsmonitor`
|
||||
is set to `true`.
|
||||
|
||||
@@ -76,9 +76,9 @@ repositories; this may be overridden by setting `fsmonitor.allowRemote` to
|
||||
correctly with all network-mounted repositories, so such use is considered
|
||||
experimental.
|
||||
|
||||
On Mac OS, the inter-process communication (IPC) between various Git
|
||||
On Mac OS and Linux, the inter-process communication (IPC) between various Git
|
||||
commands and the fsmonitor daemon is done via a Unix domain socket (UDS) -- a
|
||||
special type of file -- which is supported by native Mac OS filesystems,
|
||||
special type of file -- which is supported by native Mac OS and Linux filesystems,
|
||||
but not on network-mounted filesystems, NTFS, or FAT32. Other filesystems
|
||||
may or may not have the needed support; the fsmonitor daemon is not guaranteed
|
||||
to work with these filesystems and such use is considered experimental.
|
||||
@@ -87,13 +87,33 @@ By default, the socket is created in the `.git` directory. However, if the
|
||||
`.git` directory is on a network-mounted filesystem, it will instead be
|
||||
created at `$HOME/.git-fsmonitor-*` unless `$HOME` itself is on a
|
||||
network-mounted filesystem, in which case you must set the configuration
|
||||
variable `fsmonitor.socketDir` to the path of a directory on a Mac OS native
|
||||
variable `fsmonitor.socketDir` to the path of a directory on a native
|
||||
filesystem in which to create the socket file.
|
||||
|
||||
If none of the above directories (`.git`, `$HOME`, or `fsmonitor.socketDir`)
|
||||
is on a native Mac OS file filesystem the fsmonitor daemon will report an
|
||||
is on a native filesystem the fsmonitor daemon will report an
|
||||
error that will cause the daemon and the currently running command to exit.
|
||||
|
||||
LINUX CAVEATS
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
On Linux, the fsmonitor daemon uses inotify to monitor filesystem events.
|
||||
The inotify system has per-user limits on the number of watches that can
|
||||
be created. The default limit is typically 8192 watches per user.
|
||||
|
||||
For large repositories with many directories, you may need to increase
|
||||
this limit. Check the current limit with:
|
||||
|
||||
cat /proc/sys/fs/inotify/max_user_watches
|
||||
|
||||
To temporarily increase the limit:
|
||||
|
||||
sudo sysctl fs.inotify.max_user_watches=65536
|
||||
|
||||
To make the change permanent, add to `/etc/sysctl.conf`:
|
||||
|
||||
fs.inotify.max_user_watches=65536
|
||||
|
||||
CONFIGURATION
|
||||
-------------
|
||||
|
||||
|
||||
4
Makefile
4
Makefile
@@ -2380,13 +2380,13 @@ 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
|
||||
COMPAT_OBJS += compat/fsmonitor/fsm-ipc-$(FSMONITOR_DAEMON_BACKEND).o
|
||||
endif
|
||||
|
||||
ifdef FSMONITOR_OS_SETTINGS
|
||||
COMPAT_CFLAGS += -DHAVE_FSMONITOR_OS_SETTINGS
|
||||
COMPAT_OBJS += compat/fsmonitor/fsm-ipc-$(FSMONITOR_OS_SETTINGS).o
|
||||
COMPAT_OBJS += compat/fsmonitor/fsm-settings-$(FSMONITOR_OS_SETTINGS).o
|
||||
COMPAT_OBJS += compat/fsmonitor/fsm-path-utils-$(FSMONITOR_OS_SETTINGS).o
|
||||
COMPAT_OBJS += compat/fsmonitor/fsm-path-utils-$(FSMONITOR_DAEMON_BACKEND).o
|
||||
endif
|
||||
|
||||
ifdef WITH_BREAKING_CHANGES
|
||||
|
||||
@@ -86,6 +86,8 @@ static int do_as_client__send_stop(void)
|
||||
{
|
||||
struct strbuf answer = STRBUF_INIT;
|
||||
int ret;
|
||||
int max_wait_ms = 30000;
|
||||
int elapsed_ms = 0;
|
||||
|
||||
ret = fsmonitor_ipc__send_command("quit", &answer);
|
||||
|
||||
@@ -96,8 +98,16 @@ static int do_as_client__send_stop(void)
|
||||
return ret;
|
||||
|
||||
trace2_region_enter("fsm_client", "polling-for-daemon-exit", NULL);
|
||||
while (fsmonitor_ipc__get_state() == IPC_STATE__LISTENING)
|
||||
while (fsmonitor_ipc__get_state() == IPC_STATE__LISTENING) {
|
||||
if (elapsed_ms >= max_wait_ms) {
|
||||
trace2_region_leave("fsm_client",
|
||||
"polling-for-daemon-exit", NULL);
|
||||
return error(_("daemon did not stop within %d seconds"),
|
||||
max_wait_ms / 1000);
|
||||
}
|
||||
sleep_millisec(50);
|
||||
elapsed_ms += 50;
|
||||
}
|
||||
trace2_region_leave("fsm_client", "polling-for-daemon-exit", NULL);
|
||||
|
||||
return 0;
|
||||
@@ -197,20 +207,31 @@ static enum fsmonitor_cookie_item_result with_lock__wait_for_cookie(
|
||||
unlink(cookie_pathname.buf);
|
||||
|
||||
/*
|
||||
* Technically, this is an infinite wait (well, unless another
|
||||
* thread sends us an abort). I'd like to change this to
|
||||
* use `pthread_cond_timedwait()` and return an error/timeout
|
||||
* and let the caller do the trivial response thing, but we
|
||||
* don't have that routine in our thread-utils.
|
||||
*
|
||||
* After extensive beta testing I'm not really worried about
|
||||
* this. Also note that the above open() and unlink() calls
|
||||
* will cause at least two FS events on that path, so the odds
|
||||
* of getting stuck are pretty slim.
|
||||
* Wait for the listener thread to observe the cookie file.
|
||||
* Time out after a short interval so that the client
|
||||
* does not hang forever if the filesystem does not deliver
|
||||
* events (e.g., on certain container/overlay filesystems
|
||||
* where inotify watches succeed but events never arrive).
|
||||
*/
|
||||
while (cookie->result == FCIR_INIT)
|
||||
pthread_cond_wait(&state->cookies_cond,
|
||||
&state->main_lock);
|
||||
{
|
||||
struct timeval now;
|
||||
struct timespec ts;
|
||||
int err = 0;
|
||||
|
||||
gettimeofday(&now, NULL);
|
||||
ts.tv_sec = now.tv_sec + 1;
|
||||
ts.tv_nsec = now.tv_usec * 1000;
|
||||
|
||||
while (cookie->result == FCIR_INIT && !err)
|
||||
err = pthread_cond_timedwait(&state->cookies_cond,
|
||||
&state->main_lock,
|
||||
&ts);
|
||||
if (err == ETIMEDOUT && cookie->result == FCIR_INIT) {
|
||||
trace_printf_key(&trace_fsmonitor,
|
||||
"cookie_wait timed out");
|
||||
cookie->result = FCIR_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
done:
|
||||
hashmap_remove(&state->cookies, &cookie->entry, NULL);
|
||||
@@ -671,7 +692,7 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,
|
||||
const struct fsmonitor_batch *batch;
|
||||
struct fsmonitor_batch *remainder = NULL;
|
||||
intmax_t count = 0, duplicates = 0;
|
||||
kh_str_t *shown;
|
||||
kh_str_t *shown = NULL;
|
||||
int hash_ret;
|
||||
int do_trivial = 0;
|
||||
int do_flush = 0;
|
||||
@@ -909,8 +930,6 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,
|
||||
total_response_len += payload.len;
|
||||
}
|
||||
|
||||
kh_release_str(shown);
|
||||
|
||||
pthread_mutex_lock(&state->main_lock);
|
||||
|
||||
if (token_data->client_ref_count > 0)
|
||||
@@ -954,6 +973,7 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,
|
||||
trace2_data_intmax("fsmonitor", the_repository, "response/count/duplicates", duplicates);
|
||||
|
||||
cleanup:
|
||||
kh_destroy_str(shown);
|
||||
strbuf_release(&response_token);
|
||||
strbuf_release(&requested_token_id);
|
||||
strbuf_release(&payload);
|
||||
@@ -1405,6 +1425,7 @@ static int fsmonitor_run_daemon(void)
|
||||
done:
|
||||
pthread_cond_destroy(&state.cookies_cond);
|
||||
pthread_mutex_destroy(&state.main_lock);
|
||||
hashmap_clear(&state.cookies);
|
||||
fsm_listen__dtor(&state);
|
||||
fsm_health__dtor(&state);
|
||||
|
||||
@@ -1420,7 +1441,7 @@ done:
|
||||
return err;
|
||||
}
|
||||
|
||||
static int try_to_run_foreground_daemon(int detach_console MAYBE_UNUSED)
|
||||
static int try_to_run_foreground_daemon(int detach_console)
|
||||
{
|
||||
/*
|
||||
* Technically, we don't need to probe for an existing daemon
|
||||
@@ -1440,10 +1461,21 @@ static int try_to_run_foreground_daemon(int detach_console MAYBE_UNUSED)
|
||||
fflush(stderr);
|
||||
}
|
||||
|
||||
if (detach_console) {
|
||||
#ifdef GIT_WINDOWS_NATIVE
|
||||
if (detach_console)
|
||||
FreeConsole();
|
||||
#else
|
||||
/*
|
||||
* Create a new session so that the daemon is detached
|
||||
* from the parent's process group. This prevents
|
||||
* shells with job control (e.g. bash with "set -m")
|
||||
* from waiting on the daemon when they wait for a
|
||||
* foreground command that implicitly spawned it.
|
||||
*/
|
||||
if (setsid() == -1)
|
||||
warning_errno(_("setsid failed"));
|
||||
#endif
|
||||
}
|
||||
|
||||
return !!fsmonitor_run_daemon();
|
||||
}
|
||||
@@ -1506,6 +1538,7 @@ static int try_to_start_background_daemon(void)
|
||||
cp.no_stdin = 1;
|
||||
cp.no_stdout = 1;
|
||||
cp.no_stderr = 1;
|
||||
cp.close_fd_above_stderr = 1;
|
||||
|
||||
sbgr = start_bg_command(&cp, bg_wait_cb, NULL,
|
||||
fsmonitor__start_timeout_sec);
|
||||
|
||||
33
compat/fsmonitor/fsm-health-linux.c
Normal file
33
compat/fsmonitor/fsm-health-linux.c
Normal file
@@ -0,0 +1,33 @@
|
||||
#include "git-compat-util.h"
|
||||
#include "config.h"
|
||||
#include "fsmonitor-ll.h"
|
||||
#include "fsm-health.h"
|
||||
#include "fsmonitor--daemon.h"
|
||||
|
||||
/*
|
||||
* The Linux fsmonitor implementation uses inotify which has its own
|
||||
* mechanisms for detecting filesystem unmount and other events that
|
||||
* would require the daemon to shutdown. Therefore, we don't need
|
||||
* a separate health thread like Windows does.
|
||||
*
|
||||
* These stub functions satisfy the interface requirements.
|
||||
*/
|
||||
|
||||
int fsm_health__ctor(struct fsmonitor_daemon_state *state UNUSED)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
void fsm_health__dtor(struct fsmonitor_daemon_state *state UNUSED)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
void fsm_health__loop(struct fsmonitor_daemon_state *state UNUSED)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
void fsm_health__stop_async(struct fsmonitor_daemon_state *state UNUSED)
|
||||
{
|
||||
}
|
||||
@@ -27,13 +27,15 @@ const char *fsmonitor_ipc__get_path(struct repository *r)
|
||||
if (ipc_path)
|
||||
return ipc_path;
|
||||
|
||||
|
||||
/* By default the socket file is created in the .git directory */
|
||||
if (fsmonitor__is_fs_remote(r->gitdir) < 1) {
|
||||
ipc_path = fsmonitor_ipc__get_default_path();
|
||||
return ipc_path;
|
||||
}
|
||||
|
||||
if (!r->worktree)
|
||||
BUG("repository has no worktree");
|
||||
|
||||
git_SHA1_Init(&sha1ctx);
|
||||
git_SHA1_Update(&sha1ctx, r->worktree, strlen(r->worktree));
|
||||
git_SHA1_Final(hash, &sha1ctx);
|
||||
746
compat/fsmonitor/fsm-listen-linux.c
Normal file
746
compat/fsmonitor/fsm-listen-linux.c
Normal file
@@ -0,0 +1,746 @@
|
||||
#include "git-compat-util.h"
|
||||
#include "dir.h"
|
||||
#include "fsmonitor-ll.h"
|
||||
#include "fsm-listen.h"
|
||||
#include "fsmonitor--daemon.h"
|
||||
#include "fsmonitor-path-utils.h"
|
||||
#include "gettext.h"
|
||||
#include "simple-ipc.h"
|
||||
#include "string-list.h"
|
||||
#include "trace.h"
|
||||
|
||||
#include <sys/inotify.h>
|
||||
|
||||
/*
|
||||
* Safe value to bitwise OR with rest of mask for
|
||||
* kernels that do not support IN_MASK_CREATE
|
||||
*/
|
||||
#ifndef IN_MASK_CREATE
|
||||
#define IN_MASK_CREATE 0x00000000
|
||||
#endif
|
||||
|
||||
enum shutdown_reason {
|
||||
SHUTDOWN_CONTINUE = 0,
|
||||
SHUTDOWN_STOP,
|
||||
SHUTDOWN_ERROR,
|
||||
SHUTDOWN_FORCE
|
||||
};
|
||||
|
||||
struct watch_entry {
|
||||
struct hashmap_entry ent;
|
||||
int wd;
|
||||
uint32_t cookie;
|
||||
const char *dir;
|
||||
};
|
||||
|
||||
struct rename_entry {
|
||||
struct hashmap_entry ent;
|
||||
time_t whence;
|
||||
uint32_t cookie;
|
||||
const char *dir;
|
||||
};
|
||||
|
||||
struct fsm_listen_data {
|
||||
int fd_inotify;
|
||||
enum shutdown_reason shutdown;
|
||||
struct hashmap watches;
|
||||
struct hashmap renames;
|
||||
struct hashmap revwatches;
|
||||
};
|
||||
|
||||
static int watch_entry_cmp(const void *cmp_data UNUSED,
|
||||
const struct hashmap_entry *eptr,
|
||||
const struct hashmap_entry *entry_or_key,
|
||||
const void *keydata UNUSED)
|
||||
{
|
||||
const struct watch_entry *e1, *e2;
|
||||
|
||||
e1 = container_of(eptr, const struct watch_entry, ent);
|
||||
e2 = container_of(entry_or_key, const struct watch_entry, ent);
|
||||
return e1->wd != e2->wd;
|
||||
}
|
||||
|
||||
static int revwatches_entry_cmp(const void *cmp_data UNUSED,
|
||||
const struct hashmap_entry *eptr,
|
||||
const struct hashmap_entry *entry_or_key,
|
||||
const void *keydata UNUSED)
|
||||
{
|
||||
const struct watch_entry *e1, *e2;
|
||||
|
||||
e1 = container_of(eptr, const struct watch_entry, ent);
|
||||
e2 = container_of(entry_or_key, const struct watch_entry, ent);
|
||||
return strcmp(e1->dir, e2->dir);
|
||||
}
|
||||
|
||||
static int rename_entry_cmp(const void *cmp_data UNUSED,
|
||||
const struct hashmap_entry *eptr,
|
||||
const struct hashmap_entry *entry_or_key,
|
||||
const void *keydata UNUSED)
|
||||
{
|
||||
const struct rename_entry *e1, *e2;
|
||||
|
||||
e1 = container_of(eptr, const struct rename_entry, ent);
|
||||
e2 = container_of(entry_or_key, const struct rename_entry, ent);
|
||||
return e1->cookie != e2->cookie;
|
||||
}
|
||||
|
||||
/*
|
||||
* Register an inotify watch, add watch descriptor to path mapping
|
||||
* and the reverse mapping.
|
||||
*/
|
||||
static int add_watch(const char *path, struct fsm_listen_data *data)
|
||||
{
|
||||
const char *interned = strintern(path);
|
||||
struct watch_entry *w1, *w2;
|
||||
|
||||
/* add the inotify watch, don't allow watches to be modified */
|
||||
int wd = inotify_add_watch(data->fd_inotify, interned,
|
||||
(IN_ALL_EVENTS | IN_ONLYDIR | IN_MASK_CREATE)
|
||||
^ IN_ACCESS ^ IN_CLOSE ^ IN_OPEN);
|
||||
if (wd < 0) {
|
||||
if (errno == ENOENT || errno == ENOTDIR)
|
||||
return 0; /* directory was deleted or is not a directory */
|
||||
if (errno == EEXIST)
|
||||
return 0; /* watch already exists, no action needed */
|
||||
if (errno == ENOSPC)
|
||||
return error(_("inotify watch limit reached; "
|
||||
"increase fs.inotify.max_user_watches"));
|
||||
return error_errno(_("inotify_add_watch('%s') failed"), interned);
|
||||
}
|
||||
|
||||
/* add watch descriptor -> directory mapping */
|
||||
CALLOC_ARRAY(w1, 1);
|
||||
w1->wd = wd;
|
||||
w1->dir = interned;
|
||||
hashmap_entry_init(&w1->ent, memhash(&w1->wd, sizeof(int)));
|
||||
hashmap_add(&data->watches, &w1->ent);
|
||||
|
||||
/* add directory -> watch descriptor mapping */
|
||||
CALLOC_ARRAY(w2, 1);
|
||||
w2->wd = wd;
|
||||
w2->dir = interned;
|
||||
hashmap_entry_init(&w2->ent, strhash(w2->dir));
|
||||
hashmap_add(&data->revwatches, &w2->ent);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Remove the inotify watch, the watch descriptor to path mapping
|
||||
* and the reverse mapping.
|
||||
*/
|
||||
static void remove_watch(struct watch_entry *w, struct fsm_listen_data *data)
|
||||
{
|
||||
struct watch_entry k1, k2, *w1, *w2;
|
||||
|
||||
/* remove watch, ignore error if kernel already did it */
|
||||
if (inotify_rm_watch(data->fd_inotify, w->wd) && errno != EINVAL)
|
||||
error_errno(_("inotify_rm_watch() failed"));
|
||||
|
||||
k1.wd = w->wd;
|
||||
hashmap_entry_init(&k1.ent, memhash(&k1.wd, sizeof(int)));
|
||||
w1 = hashmap_remove_entry(&data->watches, &k1, ent, NULL);
|
||||
if (!w1)
|
||||
BUG("double remove of watch for '%s'", w->dir);
|
||||
|
||||
if (w1->cookie)
|
||||
BUG("removing watch for '%s' which has a pending rename", w1->dir);
|
||||
|
||||
k2.dir = w->dir;
|
||||
hashmap_entry_init(&k2.ent, strhash(k2.dir));
|
||||
w2 = hashmap_remove_entry(&data->revwatches, &k2, ent, NULL);
|
||||
if (!w2)
|
||||
BUG("double remove of reverse watch for '%s'", w->dir);
|
||||
|
||||
/* w1->dir and w2->dir are interned strings, we don't own them */
|
||||
free(w1);
|
||||
free(w2);
|
||||
}
|
||||
|
||||
/*
|
||||
* Check for stale directory renames.
|
||||
*
|
||||
* https://man7.org/linux/man-pages/man7/inotify.7.html
|
||||
*
|
||||
* Allow for some small timeout to account for the fact that insertion of the
|
||||
* IN_MOVED_FROM+IN_MOVED_TO event pair is not atomic, and the possibility that
|
||||
* there may not be any IN_MOVED_TO event.
|
||||
*
|
||||
* If the IN_MOVED_TO event is not received within the timeout then events have
|
||||
* been missed and the monitor is in an inconsistent state with respect to the
|
||||
* filesystem.
|
||||
*/
|
||||
static int check_stale_dir_renames(struct hashmap *renames, time_t max_age)
|
||||
{
|
||||
struct rename_entry *re;
|
||||
struct hashmap_iter iter;
|
||||
|
||||
hashmap_for_each_entry(renames, &iter, re, ent) {
|
||||
if (re->whence <= max_age)
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Track pending renames.
|
||||
*
|
||||
* Tracking is done via an event cookie to watch descriptor mapping.
|
||||
*
|
||||
* A rename is not complete until matching an IN_MOVED_TO event is received
|
||||
* for a corresponding IN_MOVED_FROM event.
|
||||
*/
|
||||
static void add_dir_rename(uint32_t cookie, const char *path,
|
||||
struct fsm_listen_data *data)
|
||||
{
|
||||
struct watch_entry k, *w;
|
||||
struct rename_entry *re;
|
||||
|
||||
/* lookup the watch descriptor for the given path */
|
||||
k.dir = path;
|
||||
hashmap_entry_init(&k.ent, strhash(path));
|
||||
w = hashmap_get_entry(&data->revwatches, &k, ent, NULL);
|
||||
if (!w) {
|
||||
/*
|
||||
* This can happen in rare cases where the directory was
|
||||
* moved before we had a chance to add a watch on it.
|
||||
* Just ignore this rename.
|
||||
*/
|
||||
trace_printf_key(&trace_fsmonitor,
|
||||
"no watch found for rename from '%s'", path);
|
||||
return;
|
||||
}
|
||||
w->cookie = cookie;
|
||||
|
||||
/* add the pending rename to match against later */
|
||||
CALLOC_ARRAY(re, 1);
|
||||
re->dir = w->dir;
|
||||
re->cookie = w->cookie;
|
||||
re->whence = time(NULL);
|
||||
hashmap_entry_init(&re->ent, memhash(&re->cookie, sizeof(uint32_t)));
|
||||
hashmap_add(&data->renames, &re->ent);
|
||||
}
|
||||
|
||||
/*
|
||||
* Handle directory renames
|
||||
*
|
||||
* Once an IN_MOVED_TO event is received, lookup the rename tracking information
|
||||
* via the event cookie and use this information to update the watch.
|
||||
*/
|
||||
static void rename_dir(uint32_t cookie, const char *path,
|
||||
struct fsm_listen_data *data)
|
||||
{
|
||||
struct rename_entry rek, *re;
|
||||
struct watch_entry k, *w;
|
||||
|
||||
/* lookup a pending rename to match */
|
||||
rek.cookie = cookie;
|
||||
hashmap_entry_init(&rek.ent, memhash(&rek.cookie, sizeof(uint32_t)));
|
||||
re = hashmap_get_entry(&data->renames, &rek, ent, NULL);
|
||||
if (re) {
|
||||
k.dir = re->dir;
|
||||
hashmap_entry_init(&k.ent, strhash(k.dir));
|
||||
w = hashmap_get_entry(&data->revwatches, &k, ent, NULL);
|
||||
if (w) {
|
||||
w->cookie = 0; /* rename handled */
|
||||
remove_watch(w, data);
|
||||
if (add_watch(path, data))
|
||||
trace_printf_key(&trace_fsmonitor,
|
||||
"failed to add watch for renamed dir '%s'",
|
||||
path);
|
||||
} else {
|
||||
/* Directory was moved out of watch tree */
|
||||
trace_printf_key(&trace_fsmonitor,
|
||||
"no matching watch for rename to '%s'", path);
|
||||
}
|
||||
hashmap_remove_entry(&data->renames, &rek, ent, NULL);
|
||||
free(re);
|
||||
} else {
|
||||
/* Directory was moved from outside the watch tree */
|
||||
trace_printf_key(&trace_fsmonitor,
|
||||
"no matching cookie for rename to '%s'", path);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Recursively add watches to every directory under path
|
||||
*/
|
||||
static int register_inotify(const char *path,
|
||||
struct fsmonitor_daemon_state *state,
|
||||
struct fsmonitor_batch *batch)
|
||||
{
|
||||
DIR *dir;
|
||||
const char *rel;
|
||||
struct strbuf current = STRBUF_INIT;
|
||||
struct dirent *de;
|
||||
struct stat fs;
|
||||
int ret = -1;
|
||||
|
||||
dir = opendir(path);
|
||||
if (!dir) {
|
||||
if (errno == ENOENT || errno == ENOTDIR)
|
||||
return 0; /* directory was deleted */
|
||||
return error_errno(_("opendir('%s') failed"), path);
|
||||
}
|
||||
|
||||
while ((de = readdir_skip_dot_and_dotdot(dir)) != NULL) {
|
||||
strbuf_reset(¤t);
|
||||
strbuf_addf(¤t, "%s/%s", path, de->d_name);
|
||||
if (lstat(current.buf, &fs)) {
|
||||
if (errno == ENOENT)
|
||||
continue; /* file was deleted */
|
||||
error_errno(_("lstat('%s') failed"), current.buf);
|
||||
goto failed;
|
||||
}
|
||||
|
||||
/* recurse into directory */
|
||||
if (S_ISDIR(fs.st_mode)) {
|
||||
if (add_watch(current.buf, state->listen_data))
|
||||
goto failed;
|
||||
if (register_inotify(current.buf, state, batch))
|
||||
goto failed;
|
||||
} else if (batch) {
|
||||
rel = current.buf + state->path_worktree_watch.len + 1;
|
||||
trace_printf_key(&trace_fsmonitor, "explicitly adding '%s'", rel);
|
||||
fsmonitor_batch__add_path(batch, rel);
|
||||
}
|
||||
}
|
||||
ret = 0;
|
||||
|
||||
failed:
|
||||
strbuf_release(¤t);
|
||||
if (closedir(dir) < 0)
|
||||
return error_errno(_("closedir('%s') failed"), path);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static int em_rename_dir_from(uint32_t mask)
|
||||
{
|
||||
return ((mask & IN_ISDIR) && (mask & IN_MOVED_FROM));
|
||||
}
|
||||
|
||||
static int em_rename_dir_to(uint32_t mask)
|
||||
{
|
||||
return ((mask & IN_ISDIR) && (mask & IN_MOVED_TO));
|
||||
}
|
||||
|
||||
static int em_remove_watch(uint32_t mask)
|
||||
{
|
||||
return (mask & IN_DELETE_SELF);
|
||||
}
|
||||
|
||||
static int em_dir_renamed(uint32_t mask)
|
||||
{
|
||||
return ((mask & IN_ISDIR) && (mask & IN_MOVE));
|
||||
}
|
||||
|
||||
static int em_dir_created(uint32_t mask)
|
||||
{
|
||||
return ((mask & IN_ISDIR) && (mask & IN_CREATE));
|
||||
}
|
||||
|
||||
static int em_dir_deleted(uint32_t mask)
|
||||
{
|
||||
return ((mask & IN_ISDIR) && (mask & IN_DELETE));
|
||||
}
|
||||
|
||||
static int em_force_shutdown(uint32_t mask)
|
||||
{
|
||||
return (mask & IN_UNMOUNT) || (mask & IN_Q_OVERFLOW);
|
||||
}
|
||||
|
||||
static int em_ignore(uint32_t mask)
|
||||
{
|
||||
return (mask & IN_IGNORED) || (mask & IN_MOVE_SELF);
|
||||
}
|
||||
|
||||
static void log_mask_set(const char *path, uint32_t mask)
|
||||
{
|
||||
struct strbuf msg = STRBUF_INIT;
|
||||
|
||||
if (mask & IN_ACCESS)
|
||||
strbuf_addstr(&msg, "IN_ACCESS|");
|
||||
if (mask & IN_MODIFY)
|
||||
strbuf_addstr(&msg, "IN_MODIFY|");
|
||||
if (mask & IN_ATTRIB)
|
||||
strbuf_addstr(&msg, "IN_ATTRIB|");
|
||||
if (mask & IN_CLOSE_WRITE)
|
||||
strbuf_addstr(&msg, "IN_CLOSE_WRITE|");
|
||||
if (mask & IN_CLOSE_NOWRITE)
|
||||
strbuf_addstr(&msg, "IN_CLOSE_NOWRITE|");
|
||||
if (mask & IN_OPEN)
|
||||
strbuf_addstr(&msg, "IN_OPEN|");
|
||||
if (mask & IN_MOVED_FROM)
|
||||
strbuf_addstr(&msg, "IN_MOVED_FROM|");
|
||||
if (mask & IN_MOVED_TO)
|
||||
strbuf_addstr(&msg, "IN_MOVED_TO|");
|
||||
if (mask & IN_CREATE)
|
||||
strbuf_addstr(&msg, "IN_CREATE|");
|
||||
if (mask & IN_DELETE)
|
||||
strbuf_addstr(&msg, "IN_DELETE|");
|
||||
if (mask & IN_DELETE_SELF)
|
||||
strbuf_addstr(&msg, "IN_DELETE_SELF|");
|
||||
if (mask & IN_MOVE_SELF)
|
||||
strbuf_addstr(&msg, "IN_MOVE_SELF|");
|
||||
if (mask & IN_UNMOUNT)
|
||||
strbuf_addstr(&msg, "IN_UNMOUNT|");
|
||||
if (mask & IN_Q_OVERFLOW)
|
||||
strbuf_addstr(&msg, "IN_Q_OVERFLOW|");
|
||||
if (mask & IN_IGNORED)
|
||||
strbuf_addstr(&msg, "IN_IGNORED|");
|
||||
if (mask & IN_ISDIR)
|
||||
strbuf_addstr(&msg, "IN_ISDIR|");
|
||||
|
||||
strbuf_strip_suffix(&msg, "|");
|
||||
|
||||
trace_printf_key(&trace_fsmonitor, "inotify_event: '%s', mask=%#8.8x %s",
|
||||
path, mask, msg.buf);
|
||||
|
||||
strbuf_release(&msg);
|
||||
}
|
||||
|
||||
int fsm_listen__ctor(struct fsmonitor_daemon_state *state)
|
||||
{
|
||||
int fd;
|
||||
int ret = 0;
|
||||
struct fsm_listen_data *data;
|
||||
|
||||
CALLOC_ARRAY(data, 1);
|
||||
state->listen_data = data;
|
||||
state->listen_error_code = -1;
|
||||
data->fd_inotify = -1;
|
||||
data->shutdown = SHUTDOWN_ERROR;
|
||||
|
||||
fd = inotify_init1(O_NONBLOCK);
|
||||
if (fd < 0) {
|
||||
FREE_AND_NULL(state->listen_data);
|
||||
return error_errno(_("inotify_init1() failed"));
|
||||
}
|
||||
|
||||
data->fd_inotify = fd;
|
||||
|
||||
hashmap_init(&data->watches, watch_entry_cmp, NULL, 0);
|
||||
hashmap_init(&data->renames, rename_entry_cmp, NULL, 0);
|
||||
hashmap_init(&data->revwatches, revwatches_entry_cmp, NULL, 0);
|
||||
|
||||
if (add_watch(state->path_worktree_watch.buf, data))
|
||||
ret = -1;
|
||||
else if (register_inotify(state->path_worktree_watch.buf, state, NULL))
|
||||
ret = -1;
|
||||
else if (state->nr_paths_watching > 1) {
|
||||
if (add_watch(state->path_gitdir_watch.buf, data))
|
||||
ret = -1;
|
||||
else if (register_inotify(state->path_gitdir_watch.buf, state, NULL))
|
||||
ret = -1;
|
||||
}
|
||||
|
||||
if (!ret) {
|
||||
state->listen_error_code = 0;
|
||||
data->shutdown = SHUTDOWN_CONTINUE;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
void fsm_listen__dtor(struct fsmonitor_daemon_state *state)
|
||||
{
|
||||
struct fsm_listen_data *data;
|
||||
struct hashmap_iter iter;
|
||||
struct watch_entry *w;
|
||||
struct watch_entry **to_remove;
|
||||
size_t nr_to_remove = 0, alloc_to_remove = 0;
|
||||
size_t i;
|
||||
int fd;
|
||||
|
||||
if (!state || !state->listen_data)
|
||||
return;
|
||||
|
||||
data = state->listen_data;
|
||||
fd = data->fd_inotify;
|
||||
|
||||
/*
|
||||
* Collect all entries first, then remove them.
|
||||
* We can't modify the hashmap while iterating over it.
|
||||
*/
|
||||
to_remove = NULL;
|
||||
hashmap_for_each_entry(&data->watches, &iter, w, ent) {
|
||||
ALLOC_GROW(to_remove, nr_to_remove + 1, alloc_to_remove);
|
||||
to_remove[nr_to_remove++] = w;
|
||||
}
|
||||
|
||||
for (i = 0; i < nr_to_remove; i++) {
|
||||
to_remove[i]->cookie = 0; /* ignore any pending renames */
|
||||
remove_watch(to_remove[i], data);
|
||||
}
|
||||
free(to_remove);
|
||||
|
||||
hashmap_clear(&data->watches);
|
||||
|
||||
hashmap_clear(&data->revwatches); /* remove_watch freed the entries */
|
||||
|
||||
hashmap_clear_and_free(&data->renames, struct rename_entry, ent);
|
||||
|
||||
FREE_AND_NULL(state->listen_data);
|
||||
|
||||
if (fd >= 0 && (close(fd) < 0))
|
||||
error_errno(_("closing inotify file descriptor failed"));
|
||||
}
|
||||
|
||||
void fsm_listen__stop_async(struct fsmonitor_daemon_state *state)
|
||||
{
|
||||
if (state && state->listen_data &&
|
||||
state->listen_data->shutdown == SHUTDOWN_CONTINUE)
|
||||
state->listen_data->shutdown = SHUTDOWN_STOP;
|
||||
}
|
||||
|
||||
/*
|
||||
* Process a single inotify event and queue for publication.
|
||||
*/
|
||||
static int process_event(const char *path,
|
||||
const struct inotify_event *event,
|
||||
struct fsmonitor_batch **batch,
|
||||
struct string_list *cookie_list,
|
||||
struct fsmonitor_daemon_state *state)
|
||||
{
|
||||
const char *rel;
|
||||
const char *last_sep;
|
||||
|
||||
switch (fsmonitor_classify_path_absolute(state, path)) {
|
||||
case IS_INSIDE_DOT_GIT_WITH_COOKIE_PREFIX:
|
||||
case IS_INSIDE_GITDIR_WITH_COOKIE_PREFIX:
|
||||
/* Use just the filename of the cookie file. */
|
||||
last_sep = find_last_dir_sep(path);
|
||||
string_list_append(cookie_list,
|
||||
last_sep ? last_sep + 1 : path);
|
||||
break;
|
||||
case IS_INSIDE_DOT_GIT:
|
||||
case IS_INSIDE_GITDIR:
|
||||
break;
|
||||
case IS_DOT_GIT:
|
||||
case IS_GITDIR:
|
||||
/*
|
||||
* If .git directory is deleted or renamed away,
|
||||
* we have to quit.
|
||||
*/
|
||||
if (em_dir_deleted(event->mask)) {
|
||||
trace_printf_key(&trace_fsmonitor,
|
||||
"event: gitdir removed");
|
||||
state->listen_data->shutdown = SHUTDOWN_FORCE;
|
||||
goto done;
|
||||
}
|
||||
|
||||
if (em_dir_renamed(event->mask)) {
|
||||
trace_printf_key(&trace_fsmonitor,
|
||||
"event: gitdir renamed");
|
||||
state->listen_data->shutdown = SHUTDOWN_FORCE;
|
||||
goto done;
|
||||
}
|
||||
break;
|
||||
case IS_WORKDIR_PATH:
|
||||
/* normal events in the working directory */
|
||||
if (trace_pass_fl(&trace_fsmonitor))
|
||||
log_mask_set(path, event->mask);
|
||||
|
||||
if (!*batch)
|
||||
*batch = fsmonitor_batch__new();
|
||||
|
||||
rel = path + state->path_worktree_watch.len + 1;
|
||||
fsmonitor_batch__add_path(*batch, rel);
|
||||
|
||||
if (em_dir_deleted(event->mask))
|
||||
break;
|
||||
|
||||
/* received IN_MOVE_FROM, add tracking for expected IN_MOVE_TO */
|
||||
if (em_rename_dir_from(event->mask))
|
||||
add_dir_rename(event->cookie, path, state->listen_data);
|
||||
|
||||
/* received IN_MOVE_TO, update watch to reflect new path */
|
||||
if (em_rename_dir_to(event->mask)) {
|
||||
rename_dir(event->cookie, path, state->listen_data);
|
||||
if (register_inotify(path, state, *batch)) {
|
||||
state->listen_data->shutdown = SHUTDOWN_ERROR;
|
||||
goto done;
|
||||
}
|
||||
}
|
||||
|
||||
if (em_dir_created(event->mask)) {
|
||||
if (add_watch(path, state->listen_data)) {
|
||||
state->listen_data->shutdown = SHUTDOWN_ERROR;
|
||||
goto done;
|
||||
}
|
||||
if (register_inotify(path, state, *batch)) {
|
||||
state->listen_data->shutdown = SHUTDOWN_ERROR;
|
||||
goto done;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case IS_OUTSIDE_CONE:
|
||||
default:
|
||||
trace_printf_key(&trace_fsmonitor,
|
||||
"ignoring '%s'", path);
|
||||
break;
|
||||
}
|
||||
return 0;
|
||||
done:
|
||||
return -1;
|
||||
}
|
||||
|
||||
/*
|
||||
* Read the inotify event stream and pre-process events before further
|
||||
* processing and eventual publishing.
|
||||
*/
|
||||
static void handle_events(struct fsmonitor_daemon_state *state)
|
||||
{
|
||||
/* See https://man7.org/linux/man-pages/man7/inotify.7.html */
|
||||
char buf[4096]
|
||||
__attribute__ ((aligned(__alignof__(struct inotify_event))));
|
||||
|
||||
struct hashmap *watches = &state->listen_data->watches;
|
||||
struct fsmonitor_batch *batch = NULL;
|
||||
struct string_list cookie_list = STRING_LIST_INIT_DUP;
|
||||
struct watch_entry k, *w;
|
||||
struct strbuf path = STRBUF_INIT;
|
||||
const struct inotify_event *event;
|
||||
int fd = state->listen_data->fd_inotify;
|
||||
ssize_t len;
|
||||
char *ptr, *p;
|
||||
|
||||
for (;;) {
|
||||
len = read(fd, buf, sizeof(buf));
|
||||
if (len == -1) {
|
||||
if (errno == EAGAIN || errno == EINTR)
|
||||
goto done;
|
||||
error_errno(_("reading inotify message stream failed"));
|
||||
state->listen_data->shutdown = SHUTDOWN_ERROR;
|
||||
goto done;
|
||||
}
|
||||
|
||||
/* nothing to read */
|
||||
if (len == 0)
|
||||
goto done;
|
||||
|
||||
/* Loop over all events in the buffer. */
|
||||
for (ptr = buf; ptr < buf + len;
|
||||
ptr += sizeof(struct inotify_event) + event->len) {
|
||||
|
||||
event = (const struct inotify_event *)ptr;
|
||||
|
||||
if (em_ignore(event->mask))
|
||||
continue;
|
||||
|
||||
/* File system was unmounted or event queue overflowed */
|
||||
if (em_force_shutdown(event->mask)) {
|
||||
if (trace_pass_fl(&trace_fsmonitor))
|
||||
log_mask_set("Forcing shutdown", event->mask);
|
||||
state->listen_data->shutdown = SHUTDOWN_FORCE;
|
||||
goto done;
|
||||
}
|
||||
|
||||
k.wd = event->wd;
|
||||
hashmap_entry_init(&k.ent, memhash(&k.wd, sizeof(int)));
|
||||
|
||||
w = hashmap_get_entry(watches, &k, ent, NULL);
|
||||
if (!w) {
|
||||
/* Watch was removed, skip event */
|
||||
continue;
|
||||
}
|
||||
|
||||
/* directory watch was removed */
|
||||
if (em_remove_watch(event->mask)) {
|
||||
remove_watch(w, state->listen_data);
|
||||
continue;
|
||||
}
|
||||
|
||||
strbuf_reset(&path);
|
||||
strbuf_addf(&path, "%s/%s", w->dir, event->name);
|
||||
|
||||
p = fsmonitor__resolve_alias(path.buf, &state->alias);
|
||||
if (!p)
|
||||
p = strbuf_detach(&path, NULL);
|
||||
|
||||
if (process_event(p, event, &batch, &cookie_list, state)) {
|
||||
free(p);
|
||||
goto done;
|
||||
}
|
||||
free(p);
|
||||
}
|
||||
strbuf_reset(&path);
|
||||
fsmonitor_publish(state, batch, &cookie_list);
|
||||
string_list_clear(&cookie_list, 0);
|
||||
batch = NULL;
|
||||
}
|
||||
done:
|
||||
strbuf_release(&path);
|
||||
fsmonitor_batch__free_list(batch);
|
||||
string_list_clear(&cookie_list, 0);
|
||||
}
|
||||
|
||||
/*
|
||||
* Non-blocking read of the inotify events stream. The inotify fd is polled
|
||||
* frequently to help minimize the number of queue overflows.
|
||||
*/
|
||||
void fsm_listen__loop(struct fsmonitor_daemon_state *state)
|
||||
{
|
||||
int poll_num;
|
||||
/*
|
||||
* Interval in seconds between checks for stale directory renames.
|
||||
* A directory rename that is not completed within this window
|
||||
* (i.e. no matching IN_MOVED_TO for an IN_MOVED_FROM) indicates
|
||||
* missed events, forcing a shutdown.
|
||||
*/
|
||||
const int interval = 1;
|
||||
time_t checked = time(NULL);
|
||||
struct pollfd fds[1];
|
||||
|
||||
fds[0].fd = state->listen_data->fd_inotify;
|
||||
fds[0].events = POLLIN;
|
||||
|
||||
/*
|
||||
* Our fs event listener is now running, so it's safe to start
|
||||
* serving client requests.
|
||||
*/
|
||||
ipc_server_start_async(state->ipc_server_data);
|
||||
|
||||
for (;;) {
|
||||
switch (state->listen_data->shutdown) {
|
||||
case SHUTDOWN_CONTINUE:
|
||||
poll_num = poll(fds, 1, 50);
|
||||
if (poll_num == -1) {
|
||||
if (errno == EINTR)
|
||||
continue;
|
||||
error_errno(_("polling inotify message stream failed"));
|
||||
state->listen_data->shutdown = SHUTDOWN_ERROR;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((time(NULL) - checked) >= interval) {
|
||||
checked = time(NULL);
|
||||
if (check_stale_dir_renames(&state->listen_data->renames,
|
||||
checked - interval)) {
|
||||
trace_printf_key(&trace_fsmonitor,
|
||||
"missed IN_MOVED_TO events, forcing shutdown");
|
||||
state->listen_data->shutdown = SHUTDOWN_FORCE;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (poll_num > 0 && (fds[0].revents & POLLIN))
|
||||
handle_events(state);
|
||||
|
||||
continue;
|
||||
case SHUTDOWN_ERROR:
|
||||
state->listen_error_code = -1;
|
||||
ipc_server_stop_async(state->ipc_server_data);
|
||||
break;
|
||||
case SHUTDOWN_FORCE:
|
||||
state->listen_error_code = 0;
|
||||
ipc_server_stop_async(state->ipc_server_data);
|
||||
break;
|
||||
case SHUTDOWN_STOP:
|
||||
default:
|
||||
state->listen_error_code = 0;
|
||||
break;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
220
compat/fsmonitor/fsm-path-utils-linux.c
Normal file
220
compat/fsmonitor/fsm-path-utils-linux.c
Normal file
@@ -0,0 +1,220 @@
|
||||
#include "git-compat-util.h"
|
||||
#include "fsmonitor-ll.h"
|
||||
#include "fsmonitor-path-utils.h"
|
||||
#include "gettext.h"
|
||||
#include "trace.h"
|
||||
|
||||
#include <sys/statfs.h>
|
||||
|
||||
#ifdef HAVE_LINUX_MAGIC_H
|
||||
#include <linux/magic.h>
|
||||
#endif
|
||||
|
||||
/*
|
||||
* Filesystem magic numbers for remote filesystems.
|
||||
* Defined here if not available in linux/magic.h.
|
||||
*/
|
||||
#ifndef CIFS_SUPER_MAGIC
|
||||
#define CIFS_SUPER_MAGIC 0xff534d42
|
||||
#endif
|
||||
#ifndef SMB_SUPER_MAGIC
|
||||
#define SMB_SUPER_MAGIC 0x517b
|
||||
#endif
|
||||
#ifndef SMB2_SUPER_MAGIC
|
||||
#define SMB2_SUPER_MAGIC 0xfe534d42
|
||||
#endif
|
||||
#ifndef NFS_SUPER_MAGIC
|
||||
#define NFS_SUPER_MAGIC 0x6969
|
||||
#endif
|
||||
#ifndef AFS_SUPER_MAGIC
|
||||
#define AFS_SUPER_MAGIC 0x5346414f
|
||||
#endif
|
||||
#ifndef CODA_SUPER_MAGIC
|
||||
#define CODA_SUPER_MAGIC 0x73757245
|
||||
#endif
|
||||
#ifndef V9FS_MAGIC
|
||||
#define V9FS_MAGIC 0x01021997
|
||||
#endif
|
||||
#ifndef FUSE_SUPER_MAGIC
|
||||
#define FUSE_SUPER_MAGIC 0x65735546
|
||||
#endif
|
||||
|
||||
/*
|
||||
* Check if filesystem type is a remote filesystem.
|
||||
*/
|
||||
static int is_remote_fs(unsigned long f_type)
|
||||
{
|
||||
switch (f_type) {
|
||||
case CIFS_SUPER_MAGIC:
|
||||
case SMB_SUPER_MAGIC:
|
||||
case SMB2_SUPER_MAGIC:
|
||||
case NFS_SUPER_MAGIC:
|
||||
case AFS_SUPER_MAGIC:
|
||||
case CODA_SUPER_MAGIC:
|
||||
case FUSE_SUPER_MAGIC:
|
||||
return 1;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Get the filesystem type name for logging purposes.
|
||||
*/
|
||||
static const char *get_fs_typename(unsigned long f_type)
|
||||
{
|
||||
switch (f_type) {
|
||||
case CIFS_SUPER_MAGIC:
|
||||
return "cifs";
|
||||
case SMB_SUPER_MAGIC:
|
||||
return "smb";
|
||||
case SMB2_SUPER_MAGIC:
|
||||
return "smb2";
|
||||
case NFS_SUPER_MAGIC:
|
||||
return "nfs";
|
||||
case AFS_SUPER_MAGIC:
|
||||
return "afs";
|
||||
case CODA_SUPER_MAGIC:
|
||||
return "coda";
|
||||
case V9FS_MAGIC:
|
||||
return "9p";
|
||||
case FUSE_SUPER_MAGIC:
|
||||
return "fuse";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Find the mount point for a given path by reading /proc/mounts.
|
||||
* Returns the filesystem type for the longest matching mount point.
|
||||
*/
|
||||
static char *find_mount(const char *path, struct statfs *fs)
|
||||
{
|
||||
FILE *fp;
|
||||
struct strbuf line = STRBUF_INIT;
|
||||
struct strbuf match = STRBUF_INIT;
|
||||
struct strbuf fstype = STRBUF_INIT;
|
||||
char *result = NULL;
|
||||
struct statfs path_fs;
|
||||
|
||||
if (statfs(path, &path_fs) < 0)
|
||||
return NULL;
|
||||
|
||||
fp = fopen("/proc/mounts", "r");
|
||||
if (!fp)
|
||||
return NULL;
|
||||
|
||||
while (strbuf_getline(&line, fp) != EOF) {
|
||||
char *fields[6];
|
||||
char *p = line.buf;
|
||||
int i;
|
||||
|
||||
/* Parse mount entry: device mountpoint fstype options dump pass */
|
||||
for (i = 0; i < 6 && p; i++) {
|
||||
fields[i] = p;
|
||||
p = strchr(p, ' ');
|
||||
if (p)
|
||||
*p++ = '\0';
|
||||
}
|
||||
|
||||
if (i >= 3) {
|
||||
const char *mountpoint = fields[1];
|
||||
const char *type = fields[2];
|
||||
struct statfs mount_fs;
|
||||
|
||||
/* Check if this mount point is a prefix of our path */
|
||||
if (starts_with(path, mountpoint) &&
|
||||
(path[strlen(mountpoint)] == '/' ||
|
||||
path[strlen(mountpoint)] == '\0')) {
|
||||
/* Check if filesystem ID matches */
|
||||
if (statfs(mountpoint, &mount_fs) == 0 &&
|
||||
!memcmp(&mount_fs.f_fsid, &path_fs.f_fsid,
|
||||
sizeof(mount_fs.f_fsid))) {
|
||||
/* Keep the longest matching mount point */
|
||||
if (strlen(mountpoint) > match.len) {
|
||||
strbuf_reset(&match);
|
||||
strbuf_addstr(&match, mountpoint);
|
||||
strbuf_reset(&fstype);
|
||||
strbuf_addstr(&fstype, type);
|
||||
*fs = mount_fs;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fclose(fp);
|
||||
strbuf_release(&line);
|
||||
strbuf_release(&match);
|
||||
|
||||
if (fstype.len)
|
||||
result = strbuf_detach(&fstype, NULL);
|
||||
else
|
||||
strbuf_release(&fstype);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
int fsmonitor__get_fs_info(const char *path, struct fs_info *fs_info)
|
||||
{
|
||||
struct statfs fs;
|
||||
|
||||
if (statfs(path, &fs) == -1) {
|
||||
int saved_errno = errno;
|
||||
trace_printf_key(&trace_fsmonitor, "statfs('%s') failed: %s",
|
||||
path, strerror(saved_errno));
|
||||
errno = saved_errno;
|
||||
return -1;
|
||||
}
|
||||
|
||||
trace_printf_key(&trace_fsmonitor,
|
||||
"statfs('%s') [type 0x%08lx]",
|
||||
path, (unsigned long)fs.f_type);
|
||||
|
||||
fs_info->is_remote = is_remote_fs(fs.f_type);
|
||||
|
||||
/*
|
||||
* Try to get filesystem type from /proc/mounts for a more
|
||||
* descriptive name.
|
||||
*/
|
||||
fs_info->typename = find_mount(path, &fs);
|
||||
if (!fs_info->typename)
|
||||
fs_info->typename = xstrdup(get_fs_typename(fs.f_type));
|
||||
|
||||
trace_printf_key(&trace_fsmonitor,
|
||||
"'%s' is_remote: %d, typename: %s",
|
||||
path, fs_info->is_remote, fs_info->typename);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int fsmonitor__is_fs_remote(const char *path)
|
||||
{
|
||||
struct fs_info fs;
|
||||
|
||||
if (fsmonitor__get_fs_info(path, &fs))
|
||||
return -1;
|
||||
|
||||
free(fs.typename);
|
||||
|
||||
return fs.is_remote;
|
||||
}
|
||||
|
||||
/*
|
||||
* No-op for Linux - we don't have firmlinks like macOS.
|
||||
*/
|
||||
int fsmonitor__get_alias(const char *path UNUSED,
|
||||
struct alias_info *info UNUSED)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* No-op for Linux - we don't have firmlinks like macOS.
|
||||
*/
|
||||
char *fsmonitor__resolve_alias(const char *path UNUSED,
|
||||
const struct alias_info *info UNUSED)
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
#include "fsmonitor-settings.h"
|
||||
#include "fsmonitor-path-utils.h"
|
||||
|
||||
/*
|
||||
/*
|
||||
* 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
|
||||
@@ -22,25 +22,31 @@
|
||||
* The builtin FSMonitor uses a Unix domain socket in the .git
|
||||
* directory for IPC. These Windows drive formats do not support
|
||||
* Unix domain sockets, so mark them as incompatible for the daemon.
|
||||
*
|
||||
*/
|
||||
static enum fsmonitor_reason check_uds_volume(struct repository *r)
|
||||
{
|
||||
struct fs_info fs;
|
||||
const char *ipc_path = fsmonitor_ipc__get_path(r);
|
||||
struct strbuf path = STRBUF_INIT;
|
||||
strbuf_add(&path, ipc_path, strlen(ipc_path));
|
||||
char *path;
|
||||
char *dir;
|
||||
|
||||
if (fsmonitor__get_fs_info(dirname(path.buf), &fs) == -1) {
|
||||
strbuf_release(&path);
|
||||
/*
|
||||
* Create a copy for dirname() since it may modify its argument.
|
||||
*/
|
||||
path = xstrdup(ipc_path);
|
||||
dir = dirname(path);
|
||||
|
||||
if (fsmonitor__get_fs_info(dir, &fs) == -1) {
|
||||
free(path);
|
||||
return FSMONITOR_REASON_ERROR;
|
||||
}
|
||||
|
||||
strbuf_release(&path);
|
||||
free(path);
|
||||
|
||||
if (fs.is_remote ||
|
||||
!strcmp(fs.typename, "msdos") ||
|
||||
!strcmp(fs.typename, "ntfs")) {
|
||||
!strcmp(fs.typename, "msdos") ||
|
||||
!strcmp(fs.typename, "ntfs") ||
|
||||
!strcmp(fs.typename, "vfat")) {
|
||||
free(fs.typename);
|
||||
return FSMONITOR_REASON_NOSOCKETS;
|
||||
}
|
||||
@@ -66,3 +66,29 @@ int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex)
|
||||
return err_win_to_posix(GetLastError());
|
||||
return 0;
|
||||
}
|
||||
|
||||
int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex,
|
||||
const struct timespec *abstime)
|
||||
{
|
||||
struct timeval now;
|
||||
long long now_ms, deadline_ms;
|
||||
DWORD timeout_ms;
|
||||
|
||||
gettimeofday(&now, NULL);
|
||||
now_ms = (long long)now.tv_sec * 1000 + now.tv_usec / 1000;
|
||||
deadline_ms = (long long)abstime->tv_sec * 1000 +
|
||||
abstime->tv_nsec / 1000000;
|
||||
|
||||
if (deadline_ms <= now_ms)
|
||||
timeout_ms = 0;
|
||||
else
|
||||
timeout_ms = (DWORD)(deadline_ms - now_ms);
|
||||
|
||||
if (SleepConditionVariableCS(cond, mutex, timeout_ms) == 0) {
|
||||
DWORD err = GetLastError();
|
||||
if (err == ERROR_TIMEOUT)
|
||||
return ETIMEDOUT;
|
||||
return err_win_to_posix(err);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -64,6 +64,8 @@ int win32_pthread_join(pthread_t *thread, void **value_ptr);
|
||||
pthread_t pthread_self(void);
|
||||
|
||||
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
|
||||
int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex,
|
||||
const struct timespec *abstime);
|
||||
|
||||
static inline void NORETURN pthread_exit(void *ret)
|
||||
{
|
||||
|
||||
@@ -68,6 +68,16 @@ ifeq ($(uname_S),Linux)
|
||||
BASIC_CFLAGS += -std=c99
|
||||
endif
|
||||
LINK_FUZZ_PROGRAMS = YesPlease
|
||||
|
||||
# The builtin FSMonitor on Linux builds upon Simple-IPC. Both require
|
||||
# Unix domain sockets and PThreads.
|
||||
ifndef NO_PTHREADS
|
||||
ifndef NO_UNIX_SOCKETS
|
||||
FSMONITOR_DAEMON_BACKEND = linux
|
||||
FSMONITOR_OS_SETTINGS = unix
|
||||
BASIC_CFLAGS += -DHAVE_LINUX_MAGIC_H
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
ifeq ($(uname_S),GNU/kFreeBSD)
|
||||
HAVE_ALLOCA_H = YesPlease
|
||||
@@ -167,7 +177,7 @@ ifeq ($(uname_S),Darwin)
|
||||
ifndef NO_PTHREADS
|
||||
ifndef NO_UNIX_SOCKETS
|
||||
FSMONITOR_DAEMON_BACKEND = darwin
|
||||
FSMONITOR_OS_SETTINGS = darwin
|
||||
FSMONITOR_OS_SETTINGS = unix
|
||||
endif
|
||||
endif
|
||||
|
||||
|
||||
@@ -293,23 +293,26 @@ endif()
|
||||
|
||||
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)
|
||||
list(APPEND compat_SOURCES compat/fsmonitor/fsm-ipc-win32.c)
|
||||
list(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-win32.c)
|
||||
|
||||
add_compile_definitions(HAVE_FSMONITOR_OS_SETTINGS)
|
||||
list(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-win32.c)
|
||||
set(FSMONITOR_DAEMON_BACKEND "win32")
|
||||
set(FSMONITOR_OS_SETTINGS "win32")
|
||||
elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
|
||||
set(FSMONITOR_DAEMON_BACKEND "darwin")
|
||||
set(FSMONITOR_OS_SETTINGS "unix")
|
||||
elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux")
|
||||
set(FSMONITOR_DAEMON_BACKEND "linux")
|
||||
set(FSMONITOR_OS_SETTINGS "unix")
|
||||
add_compile_definitions(HAVE_LINUX_MAGIC_H)
|
||||
endif()
|
||||
|
||||
if(FSMONITOR_DAEMON_BACKEND)
|
||||
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)
|
||||
list(APPEND compat_SOURCES compat/fsmonitor/fsm-ipc-darwin.c)
|
||||
list(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-darwin.c)
|
||||
list(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-${FSMONITOR_DAEMON_BACKEND}.c)
|
||||
list(APPEND compat_SOURCES compat/fsmonitor/fsm-health-${FSMONITOR_DAEMON_BACKEND}.c)
|
||||
list(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-${FSMONITOR_DAEMON_BACKEND}.c)
|
||||
list(APPEND compat_SOURCES compat/fsmonitor/fsm-ipc-${FSMONITOR_OS_SETTINGS}.c)
|
||||
|
||||
add_compile_definitions(HAVE_FSMONITOR_OS_SETTINGS)
|
||||
list(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-darwin.c)
|
||||
list(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-${FSMONITOR_OS_SETTINGS}.c)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
|
||||
@@ -61,6 +61,9 @@ static int spawn_daemon(void)
|
||||
|
||||
cmd.git_cmd = 1;
|
||||
cmd.no_stdin = 1;
|
||||
cmd.no_stdout = 1;
|
||||
cmd.no_stderr = 1;
|
||||
cmd.close_fd_above_stderr = 1;
|
||||
cmd.trace2_child_class = "fsmonitor";
|
||||
strvec_pushl(&cmd.args, "fsmonitor--daemon", "start", NULL);
|
||||
|
||||
|
||||
13
meson.build
13
meson.build
@@ -1331,10 +1331,17 @@ else
|
||||
endif
|
||||
|
||||
fsmonitor_backend = ''
|
||||
fsmonitor_os = ''
|
||||
if host_machine.system() == 'windows'
|
||||
fsmonitor_backend = 'win32'
|
||||
fsmonitor_os = 'win32'
|
||||
elif host_machine.system() == 'linux' and threads.found() and compiler.has_header('linux/magic.h')
|
||||
fsmonitor_backend = 'linux'
|
||||
fsmonitor_os = 'unix'
|
||||
libgit_c_args += '-DHAVE_LINUX_MAGIC_H'
|
||||
elif host_machine.system() == 'darwin'
|
||||
fsmonitor_backend = 'darwin'
|
||||
fsmonitor_os = 'unix'
|
||||
libgit_dependencies += dependency('CoreServices')
|
||||
endif
|
||||
if fsmonitor_backend != ''
|
||||
@@ -1343,14 +1350,14 @@ if fsmonitor_backend != ''
|
||||
|
||||
libgit_sources += [
|
||||
'compat/fsmonitor/fsm-health-' + fsmonitor_backend + '.c',
|
||||
'compat/fsmonitor/fsm-ipc-' + fsmonitor_backend + '.c',
|
||||
'compat/fsmonitor/fsm-listen-' + fsmonitor_backend + '.c',
|
||||
'compat/fsmonitor/fsm-path-utils-' + fsmonitor_backend + '.c',
|
||||
'compat/fsmonitor/fsm-settings-' + fsmonitor_backend + '.c',
|
||||
'compat/fsmonitor/fsm-ipc-' + fsmonitor_os + '.c',
|
||||
'compat/fsmonitor/fsm-settings-' + fsmonitor_os + '.c',
|
||||
]
|
||||
endif
|
||||
build_options_config.set_quoted('FSMONITOR_DAEMON_BACKEND', fsmonitor_backend)
|
||||
build_options_config.set_quoted('FSMONITOR_OS_SETTINGS', fsmonitor_backend)
|
||||
build_options_config.set_quoted('FSMONITOR_OS_SETTINGS', fsmonitor_os)
|
||||
|
||||
if not get_option('b_sanitize').contains('address') and get_option('regex').allowed() and compiler.has_header('regex.h') and compiler.get_define('REG_STARTEND', prefix: '#include <regex.h>') != ''
|
||||
build_options_config.set('NO_REGEX', '')
|
||||
|
||||
@@ -832,6 +832,17 @@ fail_pipe:
|
||||
child_close(cmd->out);
|
||||
}
|
||||
|
||||
if (cmd->close_fd_above_stderr) {
|
||||
long max_fd = sysconf(_SC_OPEN_MAX);
|
||||
int fd;
|
||||
if (max_fd < 0 || max_fd > 4096)
|
||||
max_fd = 4096;
|
||||
for (fd = 3; fd < max_fd; fd++) {
|
||||
if (fd != child_notifier)
|
||||
close(fd);
|
||||
}
|
||||
}
|
||||
|
||||
if (cmd->dir && chdir(cmd->dir))
|
||||
child_die(CHILD_ERR_CHDIR);
|
||||
|
||||
|
||||
@@ -141,6 +141,15 @@ struct child_process {
|
||||
unsigned stdout_to_stderr:1;
|
||||
unsigned clean_on_exit:1;
|
||||
unsigned wait_after_clean:1;
|
||||
|
||||
/**
|
||||
* Close file descriptors 3 and above in the child after forking
|
||||
* but before exec. This prevents the long-running child from
|
||||
* inheriting pipe endpoints or other descriptors from the parent
|
||||
* environment (e.g., the test harness).
|
||||
*/
|
||||
unsigned close_fd_above_stderr:1;
|
||||
|
||||
void (*clean_on_exit_handler)(struct child_process *process);
|
||||
};
|
||||
|
||||
|
||||
@@ -10,9 +10,58 @@ then
|
||||
test_done
|
||||
fi
|
||||
|
||||
# Verify that the filesystem delivers events to the daemon.
|
||||
# On some configurations (e.g., overlayfs with older kernels),
|
||||
# inotify watches succeed but events are never delivered. The
|
||||
# cookie wait will time out and the daemon logs a trace message.
|
||||
#
|
||||
# Use "timeout" (if available) to guard each step against hangs.
|
||||
maybe_timeout () {
|
||||
if type timeout >/dev/null 2>&1
|
||||
then
|
||||
timeout "$@"
|
||||
else
|
||||
shift
|
||||
"$@"
|
||||
fi
|
||||
}
|
||||
verify_fsmonitor_works () {
|
||||
git init test_fsmonitor_smoke || return 1
|
||||
|
||||
GIT_TRACE_FSMONITOR="$PWD/smoke.trace" &&
|
||||
export GIT_TRACE_FSMONITOR &&
|
||||
maybe_timeout 30 \
|
||||
git -C test_fsmonitor_smoke fsmonitor--daemon start \
|
||||
--start-timeout=10
|
||||
ret=$?
|
||||
unset GIT_TRACE_FSMONITOR
|
||||
if test $ret -ne 0
|
||||
then
|
||||
rm -rf test_fsmonitor_smoke smoke.trace
|
||||
return 1
|
||||
fi
|
||||
|
||||
maybe_timeout 10 \
|
||||
test-tool -C test_fsmonitor_smoke fsmonitor-client query \
|
||||
--token 0 >/dev/null 2>&1
|
||||
maybe_timeout 5 \
|
||||
git -C test_fsmonitor_smoke fsmonitor--daemon stop 2>/dev/null
|
||||
! grep -q "cookie_wait timed out" "$PWD/smoke.trace" 2>/dev/null
|
||||
ret=$?
|
||||
rm -rf test_fsmonitor_smoke smoke.trace
|
||||
return $ret
|
||||
}
|
||||
|
||||
if ! verify_fsmonitor_works
|
||||
then
|
||||
skip_all="filesystem does not deliver fsmonitor events (container/overlayfs?)"
|
||||
test_done
|
||||
fi
|
||||
|
||||
stop_daemon_delete_repo () {
|
||||
r=$1 &&
|
||||
test_might_fail git -C $r fsmonitor--daemon stop &&
|
||||
test_might_fail maybe_timeout 30 \
|
||||
git -C $r fsmonitor--daemon stop 2>/dev/null
|
||||
rm -rf $1
|
||||
}
|
||||
|
||||
@@ -67,7 +116,7 @@ start_daemon () {
|
||||
export GIT_TEST_FSMONITOR_TOKEN
|
||||
fi &&
|
||||
|
||||
git $r fsmonitor--daemon start &&
|
||||
git $r fsmonitor--daemon start --start-timeout=10 &&
|
||||
git $r fsmonitor--daemon status
|
||||
)
|
||||
}
|
||||
@@ -523,6 +572,28 @@ test_expect_success 'directory changes to a file' '
|
||||
retry_grep "^event: dir1$" .git/trace
|
||||
'
|
||||
|
||||
test_expect_success 'rapid nested directory creation' '
|
||||
test_when_finished "git fsmonitor--daemon stop; rm -rf rapid" &&
|
||||
|
||||
start_daemon --tf "$PWD/.git/trace" &&
|
||||
|
||||
# Rapidly create nested directories to exercise race conditions
|
||||
# where directory watches may be added concurrently during
|
||||
# event processing and recursive scanning.
|
||||
for i in $(test_seq 1 20)
|
||||
do
|
||||
mkdir -p "rapid/nested/dir$i/subdir/deep" || return 1
|
||||
done &&
|
||||
|
||||
# Give the daemon time to process all events
|
||||
sleep 1 &&
|
||||
|
||||
test-tool fsmonitor-client query --token 0 &&
|
||||
|
||||
# Verify daemon is still running (did not crash)
|
||||
git fsmonitor--daemon status
|
||||
'
|
||||
|
||||
# The next few test cases exercise the token-resync code. When filesystem
|
||||
# drops events (because of filesystem velocity or because the daemon isn't
|
||||
# polling fast enough), we need to discard the cached data (relative to the
|
||||
@@ -698,7 +769,7 @@ do
|
||||
else
|
||||
test_expect_success "Matrix[uc:$uc_val][fsm:$fsm_val] enable fsmonitor" '
|
||||
git config core.fsmonitor true &&
|
||||
git fsmonitor--daemon start &&
|
||||
git fsmonitor--daemon start --start-timeout=10 &&
|
||||
git update-index --fsmonitor
|
||||
'
|
||||
fi
|
||||
@@ -913,7 +984,10 @@ test_expect_success "submodule absorbgitdirs implicitly starts daemon" '
|
||||
start_git_in_background () {
|
||||
git "$@" &
|
||||
git_pid=$!
|
||||
git_pgid=$(ps -o pgid= -p $git_pid)
|
||||
git_pgid=$(ps -o pgid= -p $git_pid 2>/dev/null ||
|
||||
awk '{print $5}' /proc/$git_pid/stat 2>/dev/null) &&
|
||||
git_pgid="${git_pgid## }" &&
|
||||
git_pgid="${git_pgid%% }"
|
||||
nr_tries_left=10
|
||||
while true
|
||||
do
|
||||
@@ -924,15 +998,26 @@ start_git_in_background () {
|
||||
fi
|
||||
sleep 1
|
||||
nr_tries_left=$(($nr_tries_left - 1))
|
||||
done >/dev/null 2>&1 &
|
||||
done >/dev/null 2>&1 3>&- 4>&- 5>&- 6>&- 7>&- &
|
||||
watchdog_pid=$!
|
||||
|
||||
# Disable job control before wait. With "set -m", bash treats
|
||||
# "wait $pid" as waiting for the entire job (process group),
|
||||
# which blocks indefinitely if the fsmonitor daemon was spawned
|
||||
# into the same process group and is still running. Turning off
|
||||
# job control makes "wait" only wait for the specific PID.
|
||||
set +m &&
|
||||
wait $git_pid
|
||||
wait_status=$?
|
||||
set -m
|
||||
return $wait_status
|
||||
}
|
||||
|
||||
stop_git () {
|
||||
while kill -0 -- -$git_pgid
|
||||
test -n "$git_pgid" || return 0
|
||||
while kill -0 -- -$git_pgid 2>/dev/null
|
||||
do
|
||||
kill -- -$git_pgid
|
||||
kill -- -$git_pgid 2>/dev/null
|
||||
sleep 1
|
||||
done
|
||||
}
|
||||
@@ -947,7 +1032,7 @@ stop_watchdog () {
|
||||
|
||||
test_expect_success !MINGW "submodule implicitly starts daemon by pull" '
|
||||
test_atexit "stop_watchdog" &&
|
||||
test_when_finished "stop_git; rm -rf cloned super sub" &&
|
||||
test_when_finished "set +m; stop_git; rm -rf cloned super sub" &&
|
||||
|
||||
create_super super &&
|
||||
create_sub sub &&
|
||||
|
||||
Reference in New Issue
Block a user