Merge pull request #3082 from dscho/fsmonitor-gfw

Add an experimental built-in FSMonitor
This commit is contained in:
Jeff Hostetler
2021-03-08 10:40:00 -05:00
committed by Johannes Schindelin
44 changed files with 7159 additions and 77 deletions

1
.gitignore vendored
View File

@@ -71,6 +71,7 @@
/git-format-patch
/git-fsck
/git-fsck-objects
/git-fsmonitor--daemon
/git-gc
/git-get-tar-commit-id
/git-grep

View File

@@ -66,18 +66,45 @@ core.fsmonitor::
will identify all files that may have changed since the
requested date/time. This information is used to speed up git by
avoiding unnecessary processing of files that have not changed.
See the "fsmonitor-watchman" section of linkgit:githooks[5].
+
See the "fsmonitor-watchman" section of linkgit:githooks[5].
+
Note: FSMonitor hooks (and this config setting) are ignored if the
(experimental) built-in FSMonitor is enabled (see
`core.useBuiltinFSMonitor`).
core.fsmonitorHookVersion::
Sets the version of hook that is to be used when calling fsmonitor.
There are currently versions 1 and 2. When this is not set,
version 2 will be tried first and if it fails then version 1
will be tried. Version 1 uses a timestamp as input to determine
which files have changes since that time but some monitors
like watchman have race conditions when used with a timestamp.
Version 2 uses an opaque string so that the monitor can return
something that can be used to determine what files have changed
without race conditions.
Sets the version of hook that is to be used when calling the
FSMonitor hook (as configured via `core.fsmonitor`).
+
There are currently versions 1 and 2. When this is not set,
version 2 will be tried first and if it fails then version 1
will be tried. Version 1 uses a timestamp as input to determine
which files have changes since that time but some monitors
like watchman have race conditions when used with a timestamp.
Version 2 uses an opaque string so that the monitor can return
something that can be used to determine what files have changed
without race conditions.
+
Note: FSMonitor hooks (and this config setting) are ignored if the
built-in FSMonitor is enabled (see `core.useBuiltinFSMonitor`).
core.useBuiltinFSMonitor::
(EXPERIMENTAL) If set to true, enable the built-in filesystem
event watcher (for technical details, see
linkgit:git-fsmonitor--daemon[1]).
+
Like external (hook-based) FSMonitors, the built-in FSMonitor can speed up
Git commands that need to refresh the Git index (e.g. `git status`) in a
worktree with many files. The built-in FSMonitor facility eliminates the
need to install and maintain an external third-party monitoring tool.
+
The built-in FSMonitor is currently available only on a limited set of
supported platforms.
+
Note: if this config setting is set to `true`, any FSMonitor hook
configured via `core.fsmonitor` (and possibly `core.fsmonitorHookVersion`)
is ignored.
core.trustctime::
If false, the ctime differences between the index and the

View File

@@ -0,0 +1,107 @@
git-fsmonitor--daemon(1)
========================
NAME
----
git-fsmonitor--daemon - (EXPERIMENTAL) Builtin file system monitor daemon
SYNOPSIS
--------
[verse]
'git fsmonitor--daemon' --start
'git fsmonitor--daemon' --run
'git fsmonitor--daemon' --stop
'git fsmonitor--daemon' --is-running
'git fsmonitor--daemon' --is-supported
'git fsmonitor--daemon' --query <token>
'git fsmonitor--daemon' --query-index
'git fsmonitor--daemon' --flush
DESCRIPTION
-----------
NOTE! This command is still only an experiment, subject to change dramatically
(or even to be abandoned).
Monitors files and directories in the working directory for changes using
platform-specific file system notification facilities.
It communicates directly with commands like `git status` using the
link:technical/api-simple-ipc.html[simple IPC] interface instead of
the slower linkgit:githooks[5] interface.
OPTIONS
-------
--start::
Starts the fsmonitor daemon in the background.
--run::
Runs the fsmonitor daemon in the foreground.
--stop::
Stops the fsmonitor daemon running for the current working
directory, if present.
--is-running::
Exits with zero status if the fsmonitor daemon is watching the
current working directory.
--is-supported::
Exits with zero status if the fsmonitor daemon feature is supported
on this platform.
--query <token>::
Connects to the fsmonitor daemon (starting it if necessary) and
requests the list of changed files and directories since the
given token.
This is intended for testing purposes.
--query-index::
Read the current `<token>` from the File System Monitor index
extension (if present) and use it to query the fsmonitor daemon.
This is intended for testing purposes.
--flush::
Force the fsmonitor daemon to flush its in-memory cache and
re-sync with the file system.
This is intended for testing purposes.
REMARKS
-------
The fsmonitor daemon is a long running process that will watch a single
working directory. Commands, such as `git status`, should automatically
start it (if necessary) when `core.useBuiltinFSMonitor` is set to `true`
(see linkgit:git-config[1]).
Configure the built-in FSMonitor via `core.useBuiltinFSMonitor` in each
working directory separately, or globally via `git config --global
core.useBuiltinFSMonitor true`.
Tokens are opaque strings. They are used by the fsmonitor daemon to
mark a point in time and the associated internal state. Callers should
make no assumptions about the content of the token. In particular,
the should not assume that it is a timestamp.
Query commands send a request-token to the daemon and it responds with
a summary of the changes that have occurred since that token was
created. The daemon also returns a response-token that the client can
use in a future query.
For more information see the "File System Monitor" section in
linkgit:git-update-index[1].
CAVEATS
-------
The fsmonitor daemon does not currently know about submodules and does
not know to filter out file system events that happen within a
submodule. If fsmonitor daemon is watching a super repo and a file is
modified within the working directory of a submodule, it will report
the change (as happening against the super repo). However, the client
should properly ignore these extra events, so performance may be affected
but it should not cause an incorrect result.
GIT
---
Part of the linkgit:git[1] suite

View File

@@ -498,7 +498,9 @@ FILE SYSTEM MONITOR
This feature is intended to speed up git operations for repos that have
large working directories.
It enables git to work together with a file system monitor (see the
It enables git to work together with a file system monitor (see
linkgit:git-fsmonitor--daemon[1]
and the
"fsmonitor-watchman" section of linkgit:githooks[5]) that can
inform it as to what files have been modified. This enables git to avoid
having to lstat() every file to find modified files.

View File

@@ -584,7 +584,8 @@ fsmonitor-watchman
This hook is invoked when the configuration option `core.fsmonitor` is
set to `.git/hooks/fsmonitor-watchman` or `.git/hooks/fsmonitor-watchmanv2`
depending on the version of the hook to use.
depending on the version of the hook to use, unless overridden via
`core.useBuiltinFSMonitor` (see linkgit:git-config[1]).
Version 1 takes two arguments, a version (1) and the time in elapsed
nanoseconds since midnight, January 1, 1970.

View File

@@ -0,0 +1,105 @@
Simple-IPC API
==============
The Simple-IPC API is a collection of `ipc_` prefixed library routines
and a basic communication protocol that allow an IPC-client process to
send an application-specific IPC-request message to an IPC-server
process and receive an application-specific IPC-response message.
Communication occurs over a named pipe on Windows and a Unix domain
socket on other platforms. IPC-clients and IPC-servers rendezvous at
a previously agreed-to application-specific pathname (which is outside
the scope of this design) that is local to the computer system.
The IPC-server routines within the server application process create a
thread pool to listen for connections and receive request messages
from multiple concurrent IPC-clients. When received, these messages
are dispatched up to the server application callbacks for handling.
IPC-server routines then incrementally relay responses back to the
IPC-client.
The IPC-client routines within a client application process connect
to the IPC-server and send a request message and wait for a response.
When received, the response is returned back the caller.
For example, the `fsmonitor--daemon` feature will be built as a server
application on top of the IPC-server library routines. It will have
threads watching for file system events and a thread pool waiting for
client connections. Clients, such as `git status` will request a list
of file system events since a point in time and the server will
respond with a list of changed files and directories. The formats of
the request and response are application-specific; the IPC-client and
IPC-server routines treat them as opaque byte streams.
Comparison with sub-process model
---------------------------------
The Simple-IPC mechanism differs from the existing `sub-process.c`
model (Documentation/technical/long-running-process-protocol.txt) and
used by applications like Git-LFS. In the LFS-style sub-process model
the helper is started by the foreground process, communication happens
via a pair of file descriptors bound to the stdin/stdout of the
sub-process, the sub-process only serves the current foreground
process, and the sub-process exits when the foreground process
terminates.
In the Simple-IPC model the server is a very long-running service. It
can service many clients at the same time and has a private socket or
named pipe connection to each active client. It might be started
(on-demand) by the current client process or it might have been
started by a previous client or by the OS at boot time. The server
process is not associated with a terminal and it persists after
clients terminate. Clients do not have access to the stdin/stdout of
the server process and therefore must communicate over sockets or
named pipes.
Server startup and shutdown
---------------------------
How an application server based upon IPC-server is started is also
outside the scope of the Simple-IPC design and is a property of the
application using it. For example, the server might be started or
restarted during routine maintenance operations, or it might be
started as a system service during the system boot-up sequence, or it
might be started on-demand by a foreground Git command when needed.
Similarly, server shutdown is a property of the application using
the simple-ipc routines. For example, the server might decide to
shutdown when idle or only upon explicit request.
Simple-IPC protocol
-------------------
The Simple-IPC protocol consists of a single request message from the
client and an optional response message from the server. Both the
client and server messages are unlimited in length and are terminated
with a flush packet.
The pkt-line routines (Documentation/technical/protocol-common.txt)
are used to simplify buffer management during message generation,
transmission, and reception. A flush packet is used to mark the end
of the message. This allows the sender to incrementally generate and
transmit the message. It allows the receiver to incrementally receive
the message in chunks and to know when they have received the entire
message.
The actual byte format of the client request and server response
messages are application specific. The IPC layer transmits and
receives them as opaque byte buffers without any concern for the
content within. It is the job of the calling application layer to
understand the contents of the request and response messages.
Summary
-------
Conceptually, the Simple-IPC protocol is similar to an HTTP REST
request. Clients connect, make an application-specific and
stateless request, receive an application-specific
response, and disconnect. It is a one round trip facility for
querying the server. The Simple-IPC routines hide the socket,
named pipe, and thread pool details and allow the application
layer to focus on the application at hand.

View File

@@ -467,6 +467,11 @@ all::
# directory, and the JSON compilation database 'compile_commands.json' will be
# created at the root of the repository.
#
# If your platform supports an built-in fsmonitor backend, set
# FSMONITOR_DAEMON_BACKEND to the name of the corresponding
# `compat/fsmonitor/fsmonitor-fs-listen-<name>.c` that implements the
# `fsmonitor_fs_listen__*()` routines.
#
# Define DEVELOPER to enable more compiler warnings. Compiler version
# and family are auto detected, but could be overridden by defining
# COMPILER_FEATURES (see config.mak.dev). You can still set
@@ -737,6 +742,7 @@ TEST_BUILTINS_OBJS += test-serve-v2.o
TEST_BUILTINS_OBJS += test-sha1.o
TEST_BUILTINS_OBJS += test-sha256.o
TEST_BUILTINS_OBJS += test-sigchain.o
TEST_BUILTINS_OBJS += test-simple-ipc.o
TEST_BUILTINS_OBJS += test-strcmp-offset.o
TEST_BUILTINS_OBJS += test-string-list.o
TEST_BUILTINS_OBJS += test-submodule-config.o
@@ -883,6 +889,7 @@ LIB_OBJS += fetch-pack.o
LIB_OBJS += fmt-merge-msg.o
LIB_OBJS += fsck.o
LIB_OBJS += fsmonitor.o
LIB_OBJS += fsmonitor-ipc.o
LIB_OBJS += gettext.o
LIB_OBJS += gpg-interface.o
LIB_OBJS += graph.o
@@ -1082,6 +1089,7 @@ BUILTIN_OBJS += builtin/fmt-merge-msg.o
BUILTIN_OBJS += builtin/for-each-ref.o
BUILTIN_OBJS += builtin/for-each-repo.o
BUILTIN_OBJS += builtin/fsck.o
BUILTIN_OBJS += builtin/fsmonitor--daemon.o
BUILTIN_OBJS += builtin/gc.o
BUILTIN_OBJS += builtin/get-tar-commit-id.o
BUILTIN_OBJS += builtin/grep.o
@@ -1672,6 +1680,14 @@ ifdef NO_UNIX_SOCKETS
BASIC_CFLAGS += -DNO_UNIX_SOCKETS
else
LIB_OBJS += unix-socket.o
LIB_OBJS += unix-stream-server.o
LIB_OBJS += compat/simple-ipc/ipc-shared.o
LIB_OBJS += compat/simple-ipc/ipc-unix-socket.o
endif
ifdef USE_WIN32_IPC
LIB_OBJS += compat/simple-ipc/ipc-shared.o
LIB_OBJS += compat/simple-ipc/ipc-win32.o
endif
ifdef NO_ICONV
@@ -1886,6 +1902,11 @@ ifdef NEED_ACCESS_ROOT_HANDLER
COMPAT_OBJS += compat/access.o
endif
ifdef FSMONITOR_DAEMON_BACKEND
COMPAT_CFLAGS += -DHAVE_FSMONITOR_DAEMON_BACKEND
COMPAT_OBJS += compat/fsmonitor/fsmonitor-fs-listen-$(FSMONITOR_DAEMON_BACKEND).o
endif
ifeq ($(TCLTK_PATH),)
NO_TCLTK = NoThanks
endif
@@ -2736,6 +2757,9 @@ GIT-BUILD-OPTIONS: FORCE
@echo PAGER_ENV=\''$(subst ','\'',$(subst ','\'',$(PAGER_ENV)))'\' >>$@+
@echo DC_SHA1=\''$(subst ','\'',$(subst ','\'',$(DC_SHA1)))'\' >>$@+
@echo X=\'$(X)\' >>$@+
ifdef FSMONITOR_DAEMON_BACKEND
@echo FSMONITOR_DAEMON_BACKEND=\''$(subst ','\'',$(subst ','\'',$(FSMONITOR_DAEMON_BACKEND)))'\' >>$@+
endif
ifdef TEST_OUTPUT_DIRECTORY
@echo TEST_OUTPUT_DIRECTORY=\''$(subst ','\'',$(subst ','\'',$(TEST_OUTPUT_DIRECTORY)))'\' >>$@+
endif

View File

@@ -158,6 +158,7 @@ int cmd_for_each_ref(int argc, const char **argv, const char *prefix);
int cmd_for_each_repo(int argc, const char **argv, const char *prefix);
int cmd_format_patch(int argc, const char **argv, const char *prefix);
int cmd_fsck(int argc, const char **argv, const char *prefix);
int cmd_fsmonitor__daemon(int argc, const char **argv, const char *prefix);
int cmd_gc(int argc, const char **argv, const char *prefix);
int cmd_get_tar_commit_id(int argc, const char **argv, const char *prefix);
int cmd_grep(int argc, const char **argv, const char *prefix);

View File

@@ -203,9 +203,10 @@ static int serve_cache_loop(int fd)
static void serve_cache(const char *socket_path, int debug)
{
struct unix_stream_listen_opts opts = UNIX_STREAM_LISTEN_OPTS_INIT;
int fd;
fd = unix_stream_listen(socket_path);
fd = unix_stream_listen(socket_path, &opts);
if (fd < 0)
die_errno("unable to bind to '%s'", socket_path);

View File

@@ -14,7 +14,7 @@
static int send_request(const char *socket, const struct strbuf *out)
{
int got_data = 0;
int fd = unix_stream_connect(socket);
int fd = unix_stream_connect(socket, 0);
if (fd < 0)
return -1;

1611
builtin/fsmonitor--daemon.c Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1214,14 +1214,14 @@ int cmd_update_index(int argc, const char **argv, const char *prefix)
}
if (fsmonitor > 0) {
if (git_config_get_fsmonitor() == 0)
if (repo_config_get_fsmonitor(r) == 0)
warning(_("core.fsmonitor is unset; "
"set it if you really want to "
"enable fsmonitor"));
add_fsmonitor(&the_index);
report(_("fsmonitor enabled"));
} else if (!fsmonitor) {
if (git_config_get_fsmonitor() == 1)
if (repo_config_get_fsmonitor(r) == 1)
warning(_("core.fsmonitor is set; "
"remove it if you really want to "
"disable fsmonitor"));

View File

@@ -0,0 +1,484 @@
#if defined(__GNUC__)
/*
* It is possible to #include CoreFoundation/CoreFoundation.h when compiling
* with clang, but not with GCC as of time of writing.
*
* See https://gcc.gnu.org/bugzilla/show_bug.cgi?id=93082 for details.
*/
typedef unsigned int FSEventStreamCreateFlags;
#define kFSEventStreamEventFlagNone 0x00000000
#define kFSEventStreamEventFlagMustScanSubDirs 0x00000001
#define kFSEventStreamEventFlagUserDropped 0x00000002
#define kFSEventStreamEventFlagKernelDropped 0x00000004
#define kFSEventStreamEventFlagEventIdsWrapped 0x00000008
#define kFSEventStreamEventFlagHistoryDone 0x00000010
#define kFSEventStreamEventFlagRootChanged 0x00000020
#define kFSEventStreamEventFlagMount 0x00000040
#define kFSEventStreamEventFlagUnmount 0x00000080
#define kFSEventStreamEventFlagItemCreated 0x00000100
#define kFSEventStreamEventFlagItemRemoved 0x00000200
#define kFSEventStreamEventFlagItemInodeMetaMod 0x00000400
#define kFSEventStreamEventFlagItemRenamed 0x00000800
#define kFSEventStreamEventFlagItemModified 0x00001000
#define kFSEventStreamEventFlagItemFinderInfoMod 0x00002000
#define kFSEventStreamEventFlagItemChangeOwner 0x00004000
#define kFSEventStreamEventFlagItemXattrMod 0x00008000
#define kFSEventStreamEventFlagItemIsFile 0x00010000
#define kFSEventStreamEventFlagItemIsDir 0x00020000
#define kFSEventStreamEventFlagItemIsSymlink 0x00040000
#define kFSEventStreamEventFlagOwnEvent 0x00080000
#define kFSEventStreamEventFlagItemIsHardlink 0x00100000
#define kFSEventStreamEventFlagItemIsLastHardlink 0x00200000
#define kFSEventStreamEventFlagItemCloned 0x00400000
typedef struct __FSEventStream *FSEventStreamRef;
typedef const FSEventStreamRef ConstFSEventStreamRef;
typedef unsigned int CFStringEncoding;
#define kCFStringEncodingUTF8 0x08000100
typedef const struct __CFString *CFStringRef;
typedef const struct __CFArray *CFArrayRef;
typedef const struct __CFRunLoop *CFRunLoopRef;
struct FSEventStreamContext {
long long version;
void *cb_data, *retain, *release, *copy_description;
};
typedef struct FSEventStreamContext FSEventStreamContext;
typedef unsigned int FSEventStreamEventFlags;
#define kFSEventStreamCreateFlagNoDefer 0x02
#define kFSEventStreamCreateFlagWatchRoot 0x04
#define kFSEventStreamCreateFlagFileEvents 0x10
typedef unsigned long long FSEventStreamEventId;
#define kFSEventStreamEventIdSinceNow 0xFFFFFFFFFFFFFFFFULL
typedef void (*FSEventStreamCallback)(ConstFSEventStreamRef streamRef,
void *context,
__SIZE_TYPE__ num_of_events,
void *event_paths,
const FSEventStreamEventFlags event_flags[],
const FSEventStreamEventId event_ids[]);
typedef double CFTimeInterval;
FSEventStreamRef FSEventStreamCreate(void *allocator,
FSEventStreamCallback callback,
FSEventStreamContext *context,
CFArrayRef paths_to_watch,
FSEventStreamEventId since_when,
CFTimeInterval latency,
FSEventStreamCreateFlags flags);
CFStringRef CFStringCreateWithCString(void *allocator, const char *string,
CFStringEncoding encoding);
CFArrayRef CFArrayCreate(void *allocator, const void **items, long long count,
void *callbacks);
void CFRunLoopRun(void);
void CFRunLoopStop(CFRunLoopRef run_loop);
CFRunLoopRef CFRunLoopGetCurrent(void);
extern CFStringRef kCFRunLoopDefaultMode;
void FSEventStreamScheduleWithRunLoop(FSEventStreamRef stream,
CFRunLoopRef run_loop,
CFStringRef run_loop_mode);
unsigned char FSEventStreamStart(FSEventStreamRef stream);
void FSEventStreamStop(FSEventStreamRef stream);
void FSEventStreamInvalidate(FSEventStreamRef stream);
void FSEventStreamRelease(FSEventStreamRef stream);
#else
/*
* Let Apple's headers declare `isalnum()` first, before
* Git's headers override it via a constant
*/
#include <string.h>
#include <CoreFoundation/CoreFoundation.h>
#include <CoreServices/CoreServices.h>
#endif
#include "cache.h"
#include "fsmonitor.h"
#include "fsmonitor-fs-listen.h"
#include "fsmonitor--daemon.h"
struct fsmonitor_daemon_backend_data
{
CFStringRef cfsr_worktree_path;
CFStringRef cfsr_gitdir_path;
CFArrayRef cfar_paths_to_watch;
int nr_paths_watching;
FSEventStreamRef stream;
CFRunLoopRef rl;
enum shutdown_style {
SHUTDOWN_EVENT = 0,
FORCE_SHUTDOWN,
FORCE_ERROR_STOP,
} shutdown_style;
unsigned int stream_scheduled:1;
unsigned int stream_started:1;
};
static void log_flags_set(const char *path, const FSEventStreamEventFlags flag)
{
struct strbuf msg = STRBUF_INIT;
if (flag & kFSEventStreamEventFlagMustScanSubDirs)
strbuf_addstr(&msg, "MustScanSubDirs|");
if (flag & kFSEventStreamEventFlagUserDropped)
strbuf_addstr(&msg, "UserDropped|");
if (flag & kFSEventStreamEventFlagKernelDropped)
strbuf_addstr(&msg, "KernelDropped|");
if (flag & kFSEventStreamEventFlagEventIdsWrapped)
strbuf_addstr(&msg, "EventIdsWrapped|");
if (flag & kFSEventStreamEventFlagHistoryDone)
strbuf_addstr(&msg, "HistoryDone|");
if (flag & kFSEventStreamEventFlagRootChanged)
strbuf_addstr(&msg, "RootChanged|");
if (flag & kFSEventStreamEventFlagMount)
strbuf_addstr(&msg, "Mount|");
if (flag & kFSEventStreamEventFlagUnmount)
strbuf_addstr(&msg, "Unmount|");
if (flag & kFSEventStreamEventFlagItemChangeOwner)
strbuf_addstr(&msg, "ItemChangeOwner|");
if (flag & kFSEventStreamEventFlagItemCreated)
strbuf_addstr(&msg, "ItemCreated|");
if (flag & kFSEventStreamEventFlagItemFinderInfoMod)
strbuf_addstr(&msg, "ItemFinderInfoMod|");
if (flag & kFSEventStreamEventFlagItemInodeMetaMod)
strbuf_addstr(&msg, "ItemInodeMetaMod|");
if (flag & kFSEventStreamEventFlagItemIsDir)
strbuf_addstr(&msg, "ItemIsDir|");
if (flag & kFSEventStreamEventFlagItemIsFile)
strbuf_addstr(&msg, "ItemIsFile|");
if (flag & kFSEventStreamEventFlagItemIsHardlink)
strbuf_addstr(&msg, "ItemIsHardlink|");
if (flag & kFSEventStreamEventFlagItemIsLastHardlink)
strbuf_addstr(&msg, "ItemIsLastHardlink|");
if (flag & kFSEventStreamEventFlagItemIsSymlink)
strbuf_addstr(&msg, "ItemIsSymlink|");
if (flag & kFSEventStreamEventFlagItemModified)
strbuf_addstr(&msg, "ItemModified|");
if (flag & kFSEventStreamEventFlagItemRemoved)
strbuf_addstr(&msg, "ItemRemoved|");
if (flag & kFSEventStreamEventFlagItemRenamed)
strbuf_addstr(&msg, "ItemRenamed|");
if (flag & kFSEventStreamEventFlagItemXattrMod)
strbuf_addstr(&msg, "ItemXattrMod|");
if (flag & kFSEventStreamEventFlagOwnEvent)
strbuf_addstr(&msg, "OwnEvent|");
if (flag & kFSEventStreamEventFlagItemCloned)
strbuf_addstr(&msg, "ItemCloned|");
trace_printf_key(&trace_fsmonitor, "fsevent: '%s', flags=%u %s",
path, flag, msg.buf);
strbuf_release(&msg);
}
static int ef_is_root_delete(const FSEventStreamEventFlags ef)
{
return (ef & kFSEventStreamEventFlagItemIsDir &&
ef & kFSEventStreamEventFlagItemRemoved);
}
static int ef_is_root_renamed(const FSEventStreamEventFlags ef)
{
return (ef & kFSEventStreamEventFlagItemIsDir &&
ef & kFSEventStreamEventFlagItemRenamed);
}
static void fsevent_callback(ConstFSEventStreamRef streamRef,
void *ctx,
size_t num_of_events,
void *event_paths,
const FSEventStreamEventFlags event_flags[],
const FSEventStreamEventId event_ids[])
{
struct fsmonitor_daemon_state *state = ctx;
struct fsmonitor_daemon_backend_data *data = state->backend_data;
char **paths = (char **)event_paths;
struct fsmonitor_batch *batch = NULL;
struct string_list cookie_list = STRING_LIST_INIT_DUP;
const char *path_k;
const char *slash;
int k;
/*
* Build a list of all filesystem changes into a private/local
* list and without holding any locks.
*/
for (k = 0; k < num_of_events; k++) {
/*
* On Mac, we receive an array of absolute paths.
*/
path_k = paths[k];
/*
* If you want to debug FSEvents, log them to GIT_TRACE_FSMONITOR.
* Please don't log them to Trace2.
*
* trace_printf_key(&trace_fsmonitor, "XXX '%s'", path_k);
*/
/*
* If event[k] is marked as dropped, we assume that we have
* lost sync with the filesystem and should flush our cached
* data. We need to:
*
* [1] Abort/wake any client threads waiting for a cookie and
* flush the cached state data (the current token), and
* create a new token.
*
* [2] Discard the batch that we were locally building (since
* they are conceptually relative to the just flushed
* token).
*/
if ((event_flags[k] & kFSEventStreamEventFlagKernelDropped) ||
(event_flags[k] & kFSEventStreamEventFlagUserDropped)) {
/*
* see also kFSEventStreamEventFlagMustScanSubDirs
*/
trace2_data_string("fsmonitor", NULL,
"fsm-listen/kernel", "dropped");
fsmonitor_force_resync(state);
if (fsmonitor_batch__free(batch))
BUG("batch should not have a next");
string_list_clear(&cookie_list, 0);
/*
* We assume that any events that we received
* in this callback after this dropped event
* may still be valid, so we continue rather
* than break. (And just in case there is a
* delete of ".git" hiding in there.)
*/
continue;
}
switch (fsmonitor_classify_path_absolute(state, path_k)) {
case IS_INSIDE_DOT_GIT_WITH_COOKIE_PREFIX:
case IS_INSIDE_GITDIR_WITH_COOKIE_PREFIX:
/* special case cookie files within .git or gitdir */
/* Use just the filename of the cookie file. */
slash = find_last_dir_sep(path_k);
string_list_append(&cookie_list,
slash ? slash + 1 : path_k);
break;
case IS_INSIDE_DOT_GIT:
case IS_INSIDE_GITDIR:
/* ignore all other paths inside of .git or gitdir */
break;
case IS_DOT_GIT:
case IS_GITDIR:
/*
* If .git directory is deleted or renamed away,
* we have to quit.
*/
if (ef_is_root_delete(event_flags[k])) {
trace2_data_string("fsmonitor", NULL,
"fsm-listen/gitdir",
"removed");
goto force_shutdown;
}
if (ef_is_root_renamed(event_flags[k])) {
trace2_data_string("fsmonitor", NULL,
"fsm-listen/gitdir",
"renamed");
goto force_shutdown;
}
break;
case IS_WORKDIR_PATH:
/* try to queue normal pathnames */
if (trace_pass_fl(&trace_fsmonitor))
log_flags_set(path_k, event_flags[k]);
/* fsevent could be marked as both a file and directory */
if (event_flags[k] & kFSEventStreamEventFlagItemIsFile) {
const char *rel = path_k +
state->path_worktree_watch.len + 1;
if (!batch)
batch = fsmonitor_batch__new();
fsmonitor_batch__add_path(batch, rel);
}
if (event_flags[k] & kFSEventStreamEventFlagItemIsDir) {
const char *rel = path_k +
state->path_worktree_watch.len + 1;
char *p = xstrfmt("%s/", rel);
if (!batch)
batch = fsmonitor_batch__new();
fsmonitor_batch__add_path(batch, p);
free(p);
}
break;
case IS_OUTSIDE_CONE:
default:
trace_printf_key(&trace_fsmonitor,
"ignoring '%s'", path_k);
break;
}
}
fsmonitor_publish(state, batch, &cookie_list);
string_list_clear(&cookie_list, 0);
return;
force_shutdown:
if (fsmonitor_batch__free(batch))
BUG("batch should not have a next");
string_list_clear(&cookie_list, 0);
data->shutdown_style = FORCE_SHUTDOWN;
CFRunLoopStop(data->rl);
return;
}
/*
* TODO Investigate the proper value for the `latency` argument in the call
* TODO to `FSEventStreamCreate()`. I'm not sure that this needs to be a
* TODO config setting or just something that we tune after some testing.
* TODO
* TODO With a latency of 0.1, I was seeing lots of dropped events during
* TODO the "touch 100000" files test within t/perf/p7519, but with a
* TODO latency of 0.001 I did not see any dropped events. So the "correct"
* TODO value may be somewhere in between.
* TODO
* TODO https://developer.apple.com/documentation/coreservices/1443980-fseventstreamcreate
*/
int fsmonitor_fs_listen__ctor(struct fsmonitor_daemon_state *state)
{
FSEventStreamCreateFlags flags = kFSEventStreamCreateFlagNoDefer |
kFSEventStreamCreateFlagWatchRoot |
kFSEventStreamCreateFlagFileEvents;
FSEventStreamContext ctx = {
0,
state,
NULL,
NULL,
NULL
};
struct fsmonitor_daemon_backend_data *data;
const void *dir_array[2];
data = xcalloc(1, sizeof(*data));
state->backend_data = data;
data->cfsr_worktree_path = CFStringCreateWithCString(
NULL, state->path_worktree_watch.buf, kCFStringEncodingUTF8);
dir_array[data->nr_paths_watching++] = data->cfsr_worktree_path;
if (state->nr_paths_watching > 1) {
data->cfsr_gitdir_path = CFStringCreateWithCString(
NULL, state->path_gitdir_watch.buf,
kCFStringEncodingUTF8);
dir_array[data->nr_paths_watching++] = data->cfsr_gitdir_path;
}
data->cfar_paths_to_watch = CFArrayCreate(NULL, dir_array,
data->nr_paths_watching,
NULL);
data->stream = FSEventStreamCreate(NULL, fsevent_callback, &ctx,
data->cfar_paths_to_watch,
kFSEventStreamEventIdSinceNow,
0.001, flags);
if (data->stream == NULL)
goto failed;
/*
* `data->rl` needs to be set inside the listener thread.
*/
return 0;
failed:
error("Unable to create FSEventStream.");
FREE_AND_NULL(state->backend_data);
return -1;
}
void fsmonitor_fs_listen__dtor(struct fsmonitor_daemon_state *state)
{
struct fsmonitor_daemon_backend_data *data;
if (!state || !state->backend_data)
return;
data = state->backend_data;
if (data->stream) {
if (data->stream_started)
FSEventStreamStop(data->stream);
if (data->stream_scheduled)
FSEventStreamInvalidate(data->stream);
FSEventStreamRelease(data->stream);
}
FREE_AND_NULL(state->backend_data);
}
void fsmonitor_fs_listen__stop_async(struct fsmonitor_daemon_state *state)
{
struct fsmonitor_daemon_backend_data *data;
data = state->backend_data;
data->shutdown_style = SHUTDOWN_EVENT;
CFRunLoopStop(data->rl);
}
void fsmonitor_fs_listen__loop(struct fsmonitor_daemon_state *state)
{
struct fsmonitor_daemon_backend_data *data;
data = state->backend_data;
data->rl = CFRunLoopGetCurrent();
FSEventStreamScheduleWithRunLoop(data->stream, data->rl, kCFRunLoopDefaultMode);
data->stream_scheduled = 1;
if (!FSEventStreamStart(data->stream)) {
error("Failed to start the FSEventStream");
goto force_error_stop_without_loop;
}
data->stream_started = 1;
CFRunLoopRun();
switch (data->shutdown_style) {
case FORCE_ERROR_STOP:
state->error_code = -1;
/* fall thru */
case FORCE_SHUTDOWN:
ipc_server_stop_async(state->ipc_server_data);
/* fall thru */
case SHUTDOWN_EVENT:
default:
break;
}
return;
force_error_stop_without_loop:
state->error_code = -1;
ipc_server_stop_async(state->ipc_server_data);
return;
}

View File

@@ -0,0 +1,514 @@
#include "cache.h"
#include "config.h"
#include "fsmonitor.h"
#include "fsmonitor-fs-listen.h"
#include "fsmonitor--daemon.h"
/*
* The documentation of ReadDirectoryChangesW() states that the maximum
* buffer size is 64K when the monitored directory is remote.
*
* Larger buffers may be used when the monitored directory is local and
* will help us receive events faster from the kernel and avoid dropped
* events.
*
* So we try to use a very large buffer and silently fallback to 64K if
* we get an error.
*/
#define MAX_RDCW_BUF_FALLBACK (65536)
#define MAX_RDCW_BUF (65536 * 8)
struct one_watch
{
char buffer[MAX_RDCW_BUF];
DWORD buf_len;
DWORD count;
struct strbuf path;
HANDLE hDir;
HANDLE hEvent;
OVERLAPPED overlapped;
/*
* Is there an active ReadDirectoryChangesW() call pending. If so, we
* need to later call GetOverlappedResult() and possibly CancelIoEx().
*/
BOOL is_active;
};
struct fsmonitor_daemon_backend_data
{
struct one_watch *watch_worktree;
struct one_watch *watch_gitdir;
HANDLE hEventShutdown;
HANDLE hListener[3]; /* we don't own these handles */
#define LISTENER_SHUTDOWN 0
#define LISTENER_HAVE_DATA_WORKTREE 1
#define LISTENER_HAVE_DATA_GITDIR 2
int nr_listener_handles;
};
/*
* Convert the WCHAR path from the notification into UTF8 and
* then normalize it.
*/
static int normalize_path_in_utf8(FILE_NOTIFY_INFORMATION *info,
struct strbuf *normalized_path)
{
int reserve;
int len = 0;
strbuf_reset(normalized_path);
if (!info->FileNameLength)
goto normalize;
/*
* Pre-reserve enough space in the UTF8 buffer for
* each Unicode WCHAR character to be mapped into a
* sequence of 2 UTF8 characters. That should let us
* avoid ERROR_INSUFFICIENT_BUFFER 99.9+% of the time.
*/
reserve = info->FileNameLength + 1;
strbuf_grow(normalized_path, reserve);
for (;;) {
len = WideCharToMultiByte(CP_UTF8, 0, info->FileName,
info->FileNameLength / sizeof(WCHAR),
normalized_path->buf,
strbuf_avail(normalized_path) - 1,
NULL, NULL);
if (len > 0)
goto normalize;
if (GetLastError() != ERROR_INSUFFICIENT_BUFFER) {
error("[GLE %ld] could not convert path to UTF-8: '%.*ls'",
GetLastError(),
(int)(info->FileNameLength / sizeof(WCHAR)),
info->FileName);
return -1;
}
strbuf_grow(normalized_path,
strbuf_avail(normalized_path) + reserve);
}
normalize:
strbuf_setlen(normalized_path, len);
return strbuf_normalize_path(normalized_path);
}
void fsmonitor_fs_listen__stop_async(struct fsmonitor_daemon_state *state)
{
SetEvent(state->backend_data->hListener[LISTENER_SHUTDOWN]);
}
static struct one_watch *create_watch(struct fsmonitor_daemon_state *state,
const char *path)
{
struct one_watch *watch = NULL;
DWORD desired_access = FILE_LIST_DIRECTORY;
DWORD share_mode =
FILE_SHARE_WRITE | FILE_SHARE_READ | FILE_SHARE_DELETE;
HANDLE hDir;
hDir = CreateFileA(path,
desired_access, share_mode, NULL, OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED,
NULL);
if (hDir == INVALID_HANDLE_VALUE) {
error(_("[GLE %ld] could not watch '%s'"),
GetLastError(), path);
return NULL;
}
watch = xcalloc(1, sizeof(*watch));
watch->buf_len = sizeof(watch->buffer); /* assume full MAX_RDCW_BUF */
strbuf_init(&watch->path, 0);
strbuf_addstr(&watch->path, path);
watch->hDir = hDir;
watch->hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
return watch;
}
static void destroy_watch(struct one_watch *watch)
{
if (!watch)
return;
strbuf_release(&watch->path);
if (watch->hDir != INVALID_HANDLE_VALUE)
CloseHandle(watch->hDir);
if (watch->hEvent != INVALID_HANDLE_VALUE)
CloseHandle(watch->hEvent);
free(watch);
}
static int start_rdcw_watch(struct fsmonitor_daemon_backend_data *data,
struct one_watch *watch)
{
DWORD dwNotifyFilter =
FILE_NOTIFY_CHANGE_FILE_NAME |
FILE_NOTIFY_CHANGE_DIR_NAME |
FILE_NOTIFY_CHANGE_ATTRIBUTES |
FILE_NOTIFY_CHANGE_SIZE |
FILE_NOTIFY_CHANGE_LAST_WRITE |
FILE_NOTIFY_CHANGE_CREATION;
ResetEvent(watch->hEvent);
memset(&watch->overlapped, 0, sizeof(watch->overlapped));
watch->overlapped.hEvent = watch->hEvent;
start_watch:
watch->is_active = ReadDirectoryChangesW(
watch->hDir, watch->buffer, watch->buf_len, TRUE,
dwNotifyFilter, &watch->count, &watch->overlapped, NULL);
if (!watch->is_active &&
GetLastError() == ERROR_INVALID_PARAMETER &&
watch->buf_len > MAX_RDCW_BUF_FALLBACK) {
watch->buf_len = MAX_RDCW_BUF_FALLBACK;
goto start_watch;
}
if (watch->is_active)
return 0;
error("ReadDirectoryChangedW failed on '%s' [GLE %ld]",
watch->path.buf, GetLastError());
return -1;
}
static int recv_rdcw_watch(struct one_watch *watch)
{
watch->is_active = FALSE;
if (GetOverlappedResult(watch->hDir, &watch->overlapped, &watch->count,
TRUE))
return 0;
// TODO If an external <gitdir> is deleted, the above returns an error.
// TODO I'm not sure that there's anything that we can do here other
// TODO than failing -- the <worktree>/.git link file would be broken
// TODO anyway. We might try to check for that and return a better
// TODO error message.
error("GetOverlappedResult failed on '%s' [GLE %ld]",
watch->path.buf, GetLastError());
return -1;
}
static void cancel_rdcw_watch(struct one_watch *watch)
{
DWORD count;
if (!watch || !watch->is_active)
return;
CancelIoEx(watch->hDir, &watch->overlapped);
GetOverlappedResult(watch->hDir, &watch->overlapped, &count, TRUE);
watch->is_active = FALSE;
}
/*
* Process filesystem events that happen anywhere (recursively) under the
* <worktree> root directory. For a normal working directory, this includes
* both version controlled files and the contents of the .git/ directory.
*
* If <worktree>/.git is a file, then we only see events for the file
* itself.
*/
static int process_worktree_events(struct fsmonitor_daemon_state *state)
{
struct fsmonitor_daemon_backend_data *data = state->backend_data;
struct one_watch *watch = data->watch_worktree;
struct strbuf path = STRBUF_INIT;
struct string_list cookie_list = STRING_LIST_INIT_DUP;
struct fsmonitor_batch *batch = NULL;
const char *p = watch->buffer;
/*
* If the kernel gets more events than will fit in the kernel
* buffer associated with our RDCW handle, it drops them and
* returns a count of zero. (A successful call, but with
* length zero.)
*/
if (!watch->count) {
trace2_data_string("fsmonitor", NULL, "fsm-listen/kernel",
"overflow");
fsmonitor_force_resync(state);
return LISTENER_HAVE_DATA_WORKTREE;
}
/*
* On Windows, `info` contains an "array" of paths that are
* relative to the root of whichever directory handle received
* the event.
*/
for (;;) {
FILE_NOTIFY_INFORMATION *info = (void *)p;
const char *slash;
enum fsmonitor_path_type t;
strbuf_reset(&path);
if (normalize_path_in_utf8(info, &path) == -1)
goto skip_this_path;
t = fsmonitor_classify_path_workdir_relative(path.buf);
switch (t) {
case IS_INSIDE_DOT_GIT_WITH_COOKIE_PREFIX:
/* special case cookie files within .git */
/* Use just the filename of the cookie file. */
slash = find_last_dir_sep(path.buf);
string_list_append(&cookie_list,
slash ? slash + 1 : path.buf);
break;
case IS_INSIDE_DOT_GIT:
/* ignore everything inside of "<worktree>/.git/" */
break;
case IS_DOT_GIT:
/* "<worktree>/.git" was deleted (or renamed away) */
if ((info->Action == FILE_ACTION_REMOVED) ||
(info->Action == FILE_ACTION_RENAMED_OLD_NAME)) {
trace2_data_string("fsmonitor", NULL,
"fsm-listen/dotgit",
"removed");
goto force_shutdown;
}
break;
case IS_WORKDIR_PATH:
/* queue normal pathname */
if (!batch)
batch = fsmonitor_batch__new();
fsmonitor_batch__add_path(batch, path.buf);
break;
case IS_GITDIR:
case IS_INSIDE_GITDIR:
case IS_INSIDE_GITDIR_WITH_COOKIE_PREFIX:
default:
BUG("unexpected path classification '%d' for '%s'",
t, path.buf);
goto skip_this_path;
}
skip_this_path:
if (!info->NextEntryOffset)
break;
p += info->NextEntryOffset;
}
fsmonitor_publish(state, batch, &cookie_list);
batch = NULL;
string_list_clear(&cookie_list, 0);
strbuf_release(&path);
return LISTENER_HAVE_DATA_WORKTREE;
force_shutdown:
fsmonitor_batch__free(batch);
string_list_clear(&cookie_list, 0);
strbuf_release(&path);
return LISTENER_SHUTDOWN;
}
/*
* Process filesystem events that happend anywhere (recursively) under the
* external <gitdir> (such as non-primary worktrees or submodules).
* We only care about cookie files that our client threads created here.
*
* Note that we DO NOT get filesystem events on the external <gitdir>
* itself (it is not inside something that we are watching). In particular,
* we do not get an event if the external <gitdir> is deleted.
*/
static int process_gitdir_events(struct fsmonitor_daemon_state *state)
{
struct fsmonitor_daemon_backend_data *data = state->backend_data;
struct one_watch *watch = data->watch_gitdir;
struct strbuf path = STRBUF_INIT;
struct string_list cookie_list = STRING_LIST_INIT_DUP;
const char *p = watch->buffer;
if (!watch->count) {
trace2_data_string("fsmonitor", NULL, "fsm-listen/kernel",
"overflow");
fsmonitor_force_resync(state);
return LISTENER_HAVE_DATA_GITDIR;
}
for (;;) {
FILE_NOTIFY_INFORMATION *info = (void *)p;
const char *slash;
enum fsmonitor_path_type t;
strbuf_reset(&path);
if (normalize_path_in_utf8(info, &path) == -1)
goto skip_this_path;
t = fsmonitor_classify_path_gitdir_relative(path.buf);
trace_printf_key(&trace_fsmonitor, "BBB: %s", path.buf);
switch (t) {
case IS_INSIDE_GITDIR_WITH_COOKIE_PREFIX:
/* special case cookie files within gitdir */
/* Use just the filename of the cookie file. */
slash = find_last_dir_sep(path.buf);
string_list_append(&cookie_list,
slash ? slash + 1 : path.buf);
break;
case IS_INSIDE_GITDIR:
goto skip_this_path;
default:
BUG("unexpected path classification '%d' for '%s'",
t, path.buf);
goto skip_this_path;
}
skip_this_path:
if (!info->NextEntryOffset)
break;
p += info->NextEntryOffset;
}
fsmonitor_publish(state, NULL, &cookie_list);
string_list_clear(&cookie_list, 0);
strbuf_release(&path);
return LISTENER_HAVE_DATA_GITDIR;
}
void fsmonitor_fs_listen__loop(struct fsmonitor_daemon_state *state)
{
struct fsmonitor_daemon_backend_data *data = state->backend_data;
DWORD dwWait;
state->error_code = 0;
if (start_rdcw_watch(data, data->watch_worktree) == -1)
goto force_error_stop;
if (data->watch_gitdir &&
start_rdcw_watch(data, data->watch_gitdir) == -1)
goto force_error_stop;
for (;;) {
dwWait = WaitForMultipleObjects(data->nr_listener_handles,
data->hListener,
FALSE, INFINITE);
if (dwWait == WAIT_OBJECT_0 + LISTENER_HAVE_DATA_WORKTREE) {
if (recv_rdcw_watch(data->watch_worktree) == -1)
goto force_error_stop;
if (process_worktree_events(state) == LISTENER_SHUTDOWN)
goto force_shutdown;
if (start_rdcw_watch(data, data->watch_worktree) == -1)
goto force_error_stop;
continue;
}
if (dwWait == WAIT_OBJECT_0 + LISTENER_HAVE_DATA_GITDIR) {
if (recv_rdcw_watch(data->watch_gitdir) == -1)
goto force_error_stop;
if (process_gitdir_events(state) == LISTENER_SHUTDOWN)
goto force_shutdown;
if (start_rdcw_watch(data, data->watch_gitdir) == -1)
goto force_error_stop;
continue;
}
if (dwWait == WAIT_OBJECT_0 + LISTENER_SHUTDOWN)
goto clean_shutdown;
error(_("could not read directory changes [GLE %ld]"),
GetLastError());
goto force_error_stop;
}
force_error_stop:
state->error_code = -1;
force_shutdown:
/*
* Tell the IPC thead pool to stop (which completes the await
* in the main thread (which will also signal this thread (if
* we are still alive))).
*/
ipc_server_stop_async(state->ipc_server_data);
clean_shutdown:
cancel_rdcw_watch(data->watch_worktree);
cancel_rdcw_watch(data->watch_gitdir);
}
int fsmonitor_fs_listen__ctor(struct fsmonitor_daemon_state *state)
{
struct fsmonitor_daemon_backend_data *data;
data = xcalloc(1, sizeof(*data));
data->hEventShutdown = CreateEvent(NULL, TRUE, FALSE, NULL);
data->watch_worktree = create_watch(state,
state->path_worktree_watch.buf);
if (!data->watch_worktree)
goto failed;
if (state->nr_paths_watching > 1) {
data->watch_gitdir = create_watch(state,
state->path_gitdir_watch.buf);
if (!data->watch_gitdir)
goto failed;
}
data->hListener[LISTENER_SHUTDOWN] = data->hEventShutdown;
data->nr_listener_handles++;
data->hListener[LISTENER_HAVE_DATA_WORKTREE] =
data->watch_worktree->hEvent;
data->nr_listener_handles++;
if (data->watch_gitdir) {
data->hListener[LISTENER_HAVE_DATA_GITDIR] =
data->watch_gitdir->hEvent;
data->nr_listener_handles++;
}
state->backend_data = data;
return 0;
failed:
CloseHandle(data->hEventShutdown);
destroy_watch(data->watch_worktree);
destroy_watch(data->watch_gitdir);
return -1;
}
void fsmonitor_fs_listen__dtor(struct fsmonitor_daemon_state *state)
{
struct fsmonitor_daemon_backend_data *data;
if (!state || !state->backend_data)
return;
data = state->backend_data;
CloseHandle(data->hEventShutdown);
destroy_watch(data->watch_worktree);
destroy_watch(data->watch_gitdir);
FREE_AND_NULL(state->backend_data);
}

View File

@@ -0,0 +1,49 @@
#ifndef FSMONITOR_FS_LISTEN_H
#define FSMONITOR_FS_LISTEN_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 listener thread.
* This will be called from the main thread PRIOR to staring the
* fsmonitor_fs_listener thread.
*
* Returns 0 if successful.
* Returns -1 otherwise.
*/
int fsmonitor_fs_listen__ctor(struct fsmonitor_daemon_state *state);
/*
* Cleanup platform-specific data for the fsmonitor listener thread.
* This will be called from the main thread AFTER joining the listener.
*/
void fsmonitor_fs_listen__dtor(struct fsmonitor_daemon_state *state);
/*
* The main body of the platform-specific event loop to watch for
* filesystem events. This will run in the fsmonitor_fs_listen thread.
*
* It should call `ipc_server_stop_async()` if the listener thread
* prematurely terminates (because of a filesystem error or if it
* detects that the .git directory has been deleted). (It should NOT
* 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
* with an error.
*/
void fsmonitor_fs_listen__loop(struct fsmonitor_daemon_state *state);
/*
* Gently request that the fsmonitor listener thread shutdown.
* It does not wait for it to stop. The caller should do a JOIN
* to wait for it.
*/
void fsmonitor_fs_listen__stop_async(struct fsmonitor_daemon_state *state);
#endif /* HAVE_FSMONITOR_DAEMON_BACKEND */
#endif /* FSMONITOR_FS_LISTEN_H */

View File

@@ -0,0 +1,28 @@
#include "cache.h"
#include "simple-ipc.h"
#include "strbuf.h"
#include "pkt-line.h"
#include "thread-utils.h"
#ifdef SUPPORTS_SIMPLE_IPC
int ipc_server_run(const char *path, const struct ipc_server_opts *opts,
ipc_server_application_cb *application_cb,
void *application_data)
{
struct ipc_server_data *server_data = NULL;
int ret;
ret = ipc_server_run_async(&server_data, path, opts,
application_cb, application_data);
if (ret)
return ret;
ret = ipc_server_await(server_data);
ipc_server_free(server_data);
return ret;
}
#endif /* SUPPORTS_SIMPLE_IPC */

View File

@@ -0,0 +1,986 @@
#include "cache.h"
#include "simple-ipc.h"
#include "strbuf.h"
#include "pkt-line.h"
#include "thread-utils.h"
#include "unix-socket.h"
#include "unix-stream-server.h"
#ifdef NO_UNIX_SOCKETS
#error compat/simple-ipc/ipc-unix-socket.c requires Unix sockets
#endif
enum ipc_active_state ipc_get_active_state(const char *path)
{
enum ipc_active_state state = IPC_STATE__OTHER_ERROR;
struct ipc_client_connect_options options
= IPC_CLIENT_CONNECT_OPTIONS_INIT;
struct stat st;
struct ipc_client_connection *connection_test = NULL;
options.wait_if_busy = 0;
options.wait_if_not_found = 0;
if (lstat(path, &st) == -1) {
switch (errno) {
case ENOENT:
case ENOTDIR:
return IPC_STATE__NOT_LISTENING;
default:
return IPC_STATE__INVALID_PATH;
}
}
/* also complain if a plain file is in the way */
if ((st.st_mode & S_IFMT) != S_IFSOCK)
return IPC_STATE__INVALID_PATH;
/*
* Just because the filesystem has a S_IFSOCK type inode
* at `path`, doesn't mean it that there is a server listening.
* Ping it to be sure.
*/
state = ipc_client_try_connect(path, &options, &connection_test);
ipc_client_close_connection(connection_test);
return state;
}
/*
* This value was chosen at random.
*/
#define WAIT_STEP_MS (50)
/*
* Try to connect to the server. If the server is just starting up or
* is very busy, we may not get a connection the first time.
*/
static enum ipc_active_state connect_to_server(
const char *path,
int timeout_ms,
const struct ipc_client_connect_options *options,
int *pfd)
{
int wait_ms = 50;
int k;
*pfd = -1;
for (k = 0; k < timeout_ms; k += wait_ms) {
int fd = unix_stream_connect(path, options->uds_disallow_chdir);
if (fd != -1) {
*pfd = fd;
return IPC_STATE__LISTENING;
}
if (errno == ENOENT) {
if (!options->wait_if_not_found)
return IPC_STATE__PATH_NOT_FOUND;
goto sleep_and_try_again;
}
if (errno == ETIMEDOUT) {
if (!options->wait_if_busy)
return IPC_STATE__NOT_LISTENING;
goto sleep_and_try_again;
}
if (errno == ECONNREFUSED) {
if (!options->wait_if_busy)
return IPC_STATE__NOT_LISTENING;
goto sleep_and_try_again;
}
return IPC_STATE__OTHER_ERROR;
sleep_and_try_again:
sleep_millisec(wait_ms);
}
return IPC_STATE__NOT_LISTENING;
}
/*
* A randomly chosen timeout value.
*/
#define MY_CONNECTION_TIMEOUT_MS (1000)
enum ipc_active_state ipc_client_try_connect(
const char *path,
const struct ipc_client_connect_options *options,
struct ipc_client_connection **p_connection)
{
enum ipc_active_state state = IPC_STATE__OTHER_ERROR;
int fd = -1;
*p_connection = NULL;
trace2_region_enter("ipc-client", "try-connect", NULL);
trace2_data_string("ipc-client", NULL, "try-connect/path", path);
state = connect_to_server(path, MY_CONNECTION_TIMEOUT_MS,
options, &fd);
trace2_data_intmax("ipc-client", NULL, "try-connect/state",
(intmax_t)state);
trace2_region_leave("ipc-client", "try-connect", NULL);
if (state == IPC_STATE__LISTENING) {
(*p_connection) = xcalloc(1, sizeof(struct ipc_client_connection));
(*p_connection)->fd = fd;
}
return state;
}
void ipc_client_close_connection(struct ipc_client_connection *connection)
{
if (!connection)
return;
if (connection->fd != -1)
close(connection->fd);
free(connection);
}
int ipc_client_send_command_to_connection(
struct ipc_client_connection *connection,
const char *message, struct strbuf *answer)
{
int ret = 0;
strbuf_setlen(answer, 0);
trace2_region_enter("ipc-client", "send-command", NULL);
if (write_packetized_from_buf_no_flush(message, strlen(message),
connection->fd) < 0 ||
packet_flush_gently(connection->fd) < 0) {
ret = error(_("could not send IPC command"));
goto done;
}
if (read_packetized_to_strbuf(
connection->fd, answer,
PACKET_READ_GENTLE_ON_EOF | PACKET_READ_GENTLE_ON_READ_ERROR) < 0) {
ret = error(_("could not read IPC response"));
goto done;
}
done:
trace2_region_leave("ipc-client", "send-command", NULL);
return ret;
}
int ipc_client_send_command(const char *path,
const struct ipc_client_connect_options *options,
const char *message, struct strbuf *answer)
{
int ret = -1;
enum ipc_active_state state;
struct ipc_client_connection *connection = NULL;
state = ipc_client_try_connect(path, options, &connection);
if (state != IPC_STATE__LISTENING)
return ret;
ret = ipc_client_send_command_to_connection(connection, message, answer);
ipc_client_close_connection(connection);
return ret;
}
static int set_socket_blocking_flag(int fd, int make_nonblocking)
{
int flags;
flags = fcntl(fd, F_GETFL, NULL);
if (flags < 0)
return -1;
if (make_nonblocking)
flags |= O_NONBLOCK;
else
flags &= ~O_NONBLOCK;
return fcntl(fd, F_SETFL, flags);
}
/*
* Magic numbers used to annotate callback instance data.
* These are used to help guard against accidentally passing the
* wrong instance data across multiple levels of callbacks (which
* is easy to do if there are `void*` arguments).
*/
enum magic {
MAGIC_SERVER_REPLY_DATA,
MAGIC_WORKER_THREAD_DATA,
MAGIC_ACCEPT_THREAD_DATA,
MAGIC_SERVER_DATA,
};
struct ipc_server_reply_data {
enum magic magic;
int fd;
struct ipc_worker_thread_data *worker_thread_data;
};
struct ipc_worker_thread_data {
enum magic magic;
struct ipc_worker_thread_data *next_thread;
struct ipc_server_data *server_data;
pthread_t pthread_id;
};
struct ipc_accept_thread_data {
enum magic magic;
struct ipc_server_data *server_data;
struct unix_stream_server_socket *server_socket;
int fd_send_shutdown;
int fd_wait_shutdown;
pthread_t pthread_id;
};
/*
* With unix-sockets, the conceptual "ipc-server" is implemented as a single
* controller "accept-thread" thread and a pool of "worker-thread" threads.
* The former does the usual `accept()` loop and dispatches connections
* to an idle worker thread. The worker threads wait in an idle loop for
* a new connection, communicate with the client and relay data to/from
* the `application_cb` and then wait for another connection from the
* server thread. This avoids the overhead of constantly creating and
* destroying threads.
*/
struct ipc_server_data {
enum magic magic;
ipc_server_application_cb *application_cb;
void *application_data;
struct strbuf buf_path;
struct ipc_accept_thread_data *accept_thread;
struct ipc_worker_thread_data *worker_thread_list;
pthread_mutex_t work_available_mutex;
pthread_cond_t work_available_cond;
/*
* Accepted but not yet processed client connections are kept
* in a circular buffer FIFO. The queue is empty when the
* positions are equal.
*/
int *fifo_fds;
int queue_size;
int back_pos;
int front_pos;
int shutdown_requested;
int is_stopped;
};
/*
* Remove and return the oldest queued connection.
*
* Returns -1 if empty.
*/
static int fifo_dequeue(struct ipc_server_data *server_data)
{
/* ASSERT holding mutex */
int fd;
if (server_data->back_pos == server_data->front_pos)
return -1;
fd = server_data->fifo_fds[server_data->front_pos];
server_data->fifo_fds[server_data->front_pos] = -1;
server_data->front_pos++;
if (server_data->front_pos == server_data->queue_size)
server_data->front_pos = 0;
return fd;
}
/*
* Push a new fd onto the back of the queue.
*
* Drop it and return -1 if queue is already full.
*/
static int fifo_enqueue(struct ipc_server_data *server_data, int fd)
{
/* ASSERT holding mutex */
int next_back_pos;
next_back_pos = server_data->back_pos + 1;
if (next_back_pos == server_data->queue_size)
next_back_pos = 0;
if (next_back_pos == server_data->front_pos) {
/* Queue is full. Just drop it. */
close(fd);
return -1;
}
server_data->fifo_fds[server_data->back_pos] = fd;
server_data->back_pos = next_back_pos;
return fd;
}
/*
* Wait for a connection to be queued to the FIFO and return it.
*
* Returns -1 if someone has already requested a shutdown.
*/
static int worker_thread__wait_for_connection(
struct ipc_worker_thread_data *worker_thread_data)
{
/* ASSERT NOT holding mutex */
struct ipc_server_data *server_data = worker_thread_data->server_data;
int fd = -1;
pthread_mutex_lock(&server_data->work_available_mutex);
for (;;) {
if (server_data->shutdown_requested)
break;
fd = fifo_dequeue(server_data);
if (fd >= 0)
break;
pthread_cond_wait(&server_data->work_available_cond,
&server_data->work_available_mutex);
}
pthread_mutex_unlock(&server_data->work_available_mutex);
return fd;
}
/*
* Forward declare our reply callback function so that any compiler
* errors are reported when we actually define the function (in addition
* to any errors reported when we try to pass this callback function as
* a parameter in a function call). The former are easier to understand.
*/
static ipc_server_reply_cb do_io_reply_callback;
/*
* Relay application's response message to the client process.
* (We do not flush at this point because we allow the caller
* to chunk data to the client thru us.)
*/
static int do_io_reply_callback(struct ipc_server_reply_data *reply_data,
const char *response, size_t response_len)
{
if (reply_data->magic != MAGIC_SERVER_REPLY_DATA)
BUG("reply_cb called with wrong instance data");
return write_packetized_from_buf_no_flush(response, response_len,
reply_data->fd);
}
/* A randomly chosen value. */
#define MY_WAIT_POLL_TIMEOUT_MS (10)
/*
* If the client hangs up without sending any data on the wire, just
* quietly close the socket and ignore this client.
*
* This worker thread is committed to reading the IPC request data
* from the client at the other end of this fd. Wait here for the
* client to actually put something on the wire -- because if the
* client just does a ping (connect and hangup without sending any
* data), our use of the pkt-line read routines will spew an error
* message.
*
* Return -1 if the client hung up.
* Return 0 if data (possibly incomplete) is ready.
*/
static int worker_thread__wait_for_io_start(
struct ipc_worker_thread_data *worker_thread_data,
int fd)
{
struct ipc_server_data *server_data = worker_thread_data->server_data;
struct pollfd pollfd[1];
int result;
for (;;) {
pollfd[0].fd = fd;
pollfd[0].events = POLLIN;
result = poll(pollfd, 1, MY_WAIT_POLL_TIMEOUT_MS);
if (result < 0) {
if (errno == EINTR)
continue;
goto cleanup;
}
if (result == 0) {
/* a timeout */
int in_shutdown;
pthread_mutex_lock(&server_data->work_available_mutex);
in_shutdown = server_data->shutdown_requested;
pthread_mutex_unlock(&server_data->work_available_mutex);
/*
* If a shutdown is already in progress and this
* client has not started talking yet, just drop it.
*/
if (in_shutdown)
goto cleanup;
continue;
}
if (pollfd[0].revents & POLLHUP)
goto cleanup;
if (pollfd[0].revents & POLLIN)
return 0;
goto cleanup;
}
cleanup:
close(fd);
return -1;
}
/*
* Receive the request/command from the client and pass it to the
* registered request-callback. The request-callback will compose
* a response and call our reply-callback to send it to the client.
*/
static int worker_thread__do_io(
struct ipc_worker_thread_data *worker_thread_data,
int fd)
{
/* ASSERT NOT holding lock */
struct strbuf buf = STRBUF_INIT;
struct ipc_server_reply_data reply_data;
int ret = 0;
reply_data.magic = MAGIC_SERVER_REPLY_DATA;
reply_data.worker_thread_data = worker_thread_data;
reply_data.fd = fd;
ret = read_packetized_to_strbuf(
reply_data.fd, &buf,
PACKET_READ_GENTLE_ON_EOF | PACKET_READ_GENTLE_ON_READ_ERROR);
if (ret >= 0) {
ret = worker_thread_data->server_data->application_cb(
worker_thread_data->server_data->application_data,
buf.buf, do_io_reply_callback, &reply_data);
packet_flush_gently(reply_data.fd);
}
else {
/*
* The client probably disconnected/shutdown before it
* could send a well-formed message. Ignore it.
*/
}
strbuf_release(&buf);
close(reply_data.fd);
return ret;
}
/*
* Block SIGPIPE on the current thread (so that we get EPIPE from
* write() rather than an actual signal).
*
* Note that using sigchain_push() and _pop() to control SIGPIPE
* around our IO calls is not thread safe:
* [] It uses a global stack of handler frames.
* [] It uses ALLOC_GROW() to resize it.
* [] Finally, according to the `signal(2)` man-page:
* "The effects of `signal()` in a multithreaded process are unspecified."
*/
static void thread_block_sigpipe(sigset_t *old_set)
{
sigset_t new_set;
sigemptyset(&new_set);
sigaddset(&new_set, SIGPIPE);
sigemptyset(old_set);
pthread_sigmask(SIG_BLOCK, &new_set, old_set);
}
/*
* Thread proc for an IPC worker thread. It handles a series of
* connections from clients. It pulls the next fd from the queue
* processes it, and then waits for the next client.
*
* Block SIGPIPE in this worker thread for the life of the thread.
* This avoids stray (and sometimes delayed) SIGPIPE signals caused
* by client errors and/or when we are under extremely heavy IO load.
*
* This means that the application callback will have SIGPIPE blocked.
* The callback should not change it.
*/
static void *worker_thread_proc(void *_worker_thread_data)
{
struct ipc_worker_thread_data *worker_thread_data = _worker_thread_data;
struct ipc_server_data *server_data = worker_thread_data->server_data;
sigset_t old_set;
int fd, io;
int ret;
trace2_thread_start("ipc-worker");
thread_block_sigpipe(&old_set);
for (;;) {
fd = worker_thread__wait_for_connection(worker_thread_data);
if (fd == -1)
break; /* in shutdown */
io = worker_thread__wait_for_io_start(worker_thread_data, fd);
if (io == -1)
continue; /* client hung up without sending anything */
ret = worker_thread__do_io(worker_thread_data, fd);
if (ret == SIMPLE_IPC_QUIT) {
trace2_data_string("ipc-worker", NULL, "queue_stop_async",
"application_quit");
/*
* The application layer is telling the ipc-server
* layer to shutdown.
*
* We DO NOT have a response to send to the client.
*
* Queue an async stop (to stop the other threads) and
* allow this worker thread to exit now (no sense waiting
* for the thread-pool shutdown signal).
*
* Other non-idle worker threads are allowed to finish
* responding to their current clients.
*/
ipc_server_stop_async(server_data);
break;
}
}
trace2_thread_exit();
return NULL;
}
/* A randomly chosen value. */
#define MY_ACCEPT_POLL_TIMEOUT_MS (60 * 1000)
/*
* Accept a new client connection on our socket. This uses non-blocking
* IO so that we can also wait for shutdown requests on our socket-pair
* without actually spinning on a fast timeout.
*/
static int accept_thread__wait_for_connection(
struct ipc_accept_thread_data *accept_thread_data)
{
struct pollfd pollfd[2];
int result;
for (;;) {
pollfd[0].fd = accept_thread_data->fd_wait_shutdown;
pollfd[0].events = POLLIN;
pollfd[1].fd = accept_thread_data->server_socket->fd_socket;
pollfd[1].events = POLLIN;
result = poll(pollfd, 2, MY_ACCEPT_POLL_TIMEOUT_MS);
if (result < 0) {
if (errno == EINTR)
continue;
return result;
}
if (result == 0) {
/* a timeout */
/*
* If someone deletes or force-creates a new unix
* domain socket at our path, all future clients
* will be routed elsewhere and we silently starve.
* If that happens, just queue a shutdown.
*/
if (unix_stream_server__was_stolen(
accept_thread_data->server_socket)) {
trace2_data_string("ipc-accept", NULL,
"queue_stop_async",
"socket_stolen");
ipc_server_stop_async(
accept_thread_data->server_data);
}
continue;
}
if (pollfd[0].revents & POLLIN) {
/* shutdown message queued to socketpair */
return -1;
}
if (pollfd[1].revents & POLLIN) {
/* a connection is available on server_socket */
int client_fd =
accept(accept_thread_data->server_socket->fd_socket,
NULL, NULL);
if (client_fd >= 0)
return client_fd;
/*
* An error here is unlikely -- it probably
* indicates that the connecting process has
* already dropped the connection.
*/
continue;
}
BUG("unandled poll result errno=%d r[0]=%d r[1]=%d",
errno, pollfd[0].revents, pollfd[1].revents);
}
}
/*
* Thread proc for the IPC server "accept thread". This waits for
* an incoming socket connection, appends it to the queue of available
* connections, and notifies a worker thread to process it.
*
* Block SIGPIPE in this thread for the life of the thread. This
* avoids any stray SIGPIPE signals when closing pipe fds under
* extremely heavy loads (such as when the fifo queue is full and we
* drop incomming connections).
*/
static void *accept_thread_proc(void *_accept_thread_data)
{
struct ipc_accept_thread_data *accept_thread_data = _accept_thread_data;
struct ipc_server_data *server_data = accept_thread_data->server_data;
sigset_t old_set;
trace2_thread_start("ipc-accept");
thread_block_sigpipe(&old_set);
for (;;) {
int client_fd = accept_thread__wait_for_connection(
accept_thread_data);
pthread_mutex_lock(&server_data->work_available_mutex);
if (server_data->shutdown_requested) {
pthread_mutex_unlock(&server_data->work_available_mutex);
if (client_fd >= 0)
close(client_fd);
break;
}
if (client_fd < 0) {
/* ignore transient accept() errors */
}
else {
fifo_enqueue(server_data, client_fd);
pthread_cond_broadcast(&server_data->work_available_cond);
}
pthread_mutex_unlock(&server_data->work_available_mutex);
}
trace2_thread_exit();
return NULL;
}
/*
* We can't predict the connection arrival rate relative to the worker
* processing rate, therefore we allow the "accept-thread" to queue up
* a generous number of connections, since we'd rather have the client
* not unnecessarily timeout if we can avoid it. (The assumption is
* that this will be used for FSMonitor and a few second wait on a
* connection is better than having the client timeout and do the full
* computation itself.)
*
* The FIFO queue size is set to a multiple of the worker pool size.
* This value chosen at random.
*/
#define FIFO_SCALE (100)
/*
* The backlog value for `listen(2)`. This doesn't need to huge,
* rather just large enough for our "accept-thread" to wake up and
* queue incoming connections onto the FIFO without the kernel
* dropping any.
*
* This value chosen at random.
*/
#define LISTEN_BACKLOG (50)
static int create_listener_socket(
const char *path,
const struct ipc_server_opts *ipc_opts,
struct unix_stream_server_socket **new_server_socket)
{
struct unix_stream_server_socket *server_socket = NULL;
struct unix_stream_listen_opts uslg_opts = UNIX_STREAM_LISTEN_OPTS_INIT;
int ret;
uslg_opts.listen_backlog_size = LISTEN_BACKLOG;
uslg_opts.disallow_chdir = ipc_opts->uds_disallow_chdir;
ret = unix_stream_server__create(path, &uslg_opts, -1, &server_socket);
if (ret)
return ret;
if (set_socket_blocking_flag(server_socket->fd_socket, 1)) {
int saved_errno = errno;
unix_stream_server__free(server_socket);
errno = saved_errno;
return -1;
}
*new_server_socket = server_socket;
trace2_data_string("ipc-server", NULL, "listen-with-lock", path);
return 0;
}
static int setup_listener_socket(
const char *path,
const struct ipc_server_opts *ipc_opts,
struct unix_stream_server_socket **new_server_socket)
{
int ret, saved_errno;
trace2_region_enter("ipc-server", "create-listener_socket", NULL);
ret = create_listener_socket(path, ipc_opts, new_server_socket);
saved_errno = errno;
trace2_region_leave("ipc-server", "create-listener_socket", NULL);
errno = saved_errno;
return ret;
}
/*
* Start IPC server in a pool of background threads.
*/
int ipc_server_run_async(struct ipc_server_data **returned_server_data,
const char *path, const struct ipc_server_opts *opts,
ipc_server_application_cb *application_cb,
void *application_data)
{
struct unix_stream_server_socket *server_socket = NULL;
struct ipc_server_data *server_data;
int sv[2];
int k;
int ret;
int nr_threads = opts->nr_threads;
*returned_server_data = NULL;
/*
* Create a socketpair and set sv[1] to non-blocking. This
* will used to send a shutdown message to the accept-thread
* and allows the accept-thread to wait on EITHER a client
* connection or a shutdown request without spinning.
*/
if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) < 0)
return -1;
if (set_socket_blocking_flag(sv[1], 1)) {
int saved_errno = errno;
close(sv[0]);
close(sv[1]);
errno = saved_errno;
return -1;
}
ret = setup_listener_socket(path, opts, &server_socket);
if (ret) {
int saved_errno = errno;
close(sv[0]);
close(sv[1]);
errno = saved_errno;
return ret;
}
server_data = xcalloc(1, sizeof(*server_data));
server_data->magic = MAGIC_SERVER_DATA;
server_data->application_cb = application_cb;
server_data->application_data = application_data;
strbuf_init(&server_data->buf_path, 0);
strbuf_addstr(&server_data->buf_path, path);
if (nr_threads < 1)
nr_threads = 1;
pthread_mutex_init(&server_data->work_available_mutex, NULL);
pthread_cond_init(&server_data->work_available_cond, NULL);
server_data->queue_size = nr_threads * FIFO_SCALE;
server_data->fifo_fds = xcalloc(server_data->queue_size,
sizeof(*server_data->fifo_fds));
server_data->accept_thread =
xcalloc(1, sizeof(*server_data->accept_thread));
server_data->accept_thread->magic = MAGIC_ACCEPT_THREAD_DATA;
server_data->accept_thread->server_data = server_data;
server_data->accept_thread->server_socket = server_socket;
server_data->accept_thread->fd_send_shutdown = sv[0];
server_data->accept_thread->fd_wait_shutdown = sv[1];
if (pthread_create(&server_data->accept_thread->pthread_id, NULL,
accept_thread_proc, server_data->accept_thread))
die_errno(_("could not start accept_thread '%s'"), path);
for (k = 0; k < nr_threads; k++) {
struct ipc_worker_thread_data *wtd;
wtd = xcalloc(1, sizeof(*wtd));
wtd->magic = MAGIC_WORKER_THREAD_DATA;
wtd->server_data = server_data;
if (pthread_create(&wtd->pthread_id, NULL, worker_thread_proc,
wtd)) {
if (k == 0)
die(_("could not start worker[0] for '%s'"),
path);
/*
* Limp along with the thread pool that we have.
*/
break;
}
wtd->next_thread = server_data->worker_thread_list;
server_data->worker_thread_list = wtd;
}
*returned_server_data = server_data;
return 0;
}
/*
* Gently tell the IPC server treads to shutdown.
* Can be run on any thread.
*/
int ipc_server_stop_async(struct ipc_server_data *server_data)
{
/* ASSERT NOT holding mutex */
int fd;
if (!server_data)
return 0;
trace2_region_enter("ipc-server", "server-stop-async", NULL);
pthread_mutex_lock(&server_data->work_available_mutex);
server_data->shutdown_requested = 1;
/*
* Write a byte to the shutdown socket pair to wake up the
* accept-thread.
*/
if (write(server_data->accept_thread->fd_send_shutdown, "Q", 1) < 0)
error_errno("could not write to fd_send_shutdown");
/*
* Drain the queue of existing connections.
*/
while ((fd = fifo_dequeue(server_data)) != -1)
close(fd);
/*
* Gently tell worker threads to stop processing new connections
* and exit. (This does not abort in-process conversations.)
*/
pthread_cond_broadcast(&server_data->work_available_cond);
pthread_mutex_unlock(&server_data->work_available_mutex);
trace2_region_leave("ipc-server", "server-stop-async", NULL);
return 0;
}
/*
* Wait for all IPC server threads to stop.
*/
int ipc_server_await(struct ipc_server_data *server_data)
{
pthread_join(server_data->accept_thread->pthread_id, NULL);
if (!server_data->shutdown_requested)
BUG("ipc-server: accept-thread stopped for '%s'",
server_data->buf_path.buf);
while (server_data->worker_thread_list) {
struct ipc_worker_thread_data *wtd =
server_data->worker_thread_list;
pthread_join(wtd->pthread_id, NULL);
server_data->worker_thread_list = wtd->next_thread;
free(wtd);
}
server_data->is_stopped = 1;
return 0;
}
void ipc_server_free(struct ipc_server_data *server_data)
{
struct ipc_accept_thread_data * accept_thread_data;
if (!server_data)
return;
if (!server_data->is_stopped)
BUG("cannot free ipc-server while running for '%s'",
server_data->buf_path.buf);
accept_thread_data = server_data->accept_thread;
if (accept_thread_data) {
unix_stream_server__free(accept_thread_data->server_socket);
if (accept_thread_data->fd_send_shutdown != -1)
close(accept_thread_data->fd_send_shutdown);
if (accept_thread_data->fd_wait_shutdown != -1)
close(accept_thread_data->fd_wait_shutdown);
free(server_data->accept_thread);
}
while (server_data->worker_thread_list) {
struct ipc_worker_thread_data *wtd =
server_data->worker_thread_list;
server_data->worker_thread_list = wtd->next_thread;
free(wtd);
}
pthread_cond_destroy(&server_data->work_available_cond);
pthread_mutex_destroy(&server_data->work_available_mutex);
strbuf_release(&server_data->buf_path);
free(server_data->fifo_fds);
free(server_data);
}

View File

@@ -0,0 +1,751 @@
#include "cache.h"
#include "simple-ipc.h"
#include "strbuf.h"
#include "pkt-line.h"
#include "thread-utils.h"
#ifndef GIT_WINDOWS_NATIVE
#error This file can only be compiled on Windows
#endif
static int initialize_pipe_name(const char *path, wchar_t *wpath, size_t alloc)
{
int off = 0;
struct strbuf realpath = STRBUF_INIT;
if (!strbuf_realpath(&realpath, path, 0))
return -1;
off = swprintf(wpath, alloc, L"\\\\.\\pipe\\");
if (xutftowcs(wpath + off, realpath.buf, alloc - off) < 0)
return -1;
/* Handle drive prefix */
if (wpath[off] && wpath[off + 1] == L':') {
wpath[off + 1] = L'_';
off += 2;
}
for (; wpath[off]; off++)
if (wpath[off] == L'/')
wpath[off] = L'\\';
strbuf_release(&realpath);
return 0;
}
static enum ipc_active_state get_active_state(wchar_t *pipe_path)
{
if (WaitNamedPipeW(pipe_path, NMPWAIT_USE_DEFAULT_WAIT))
return IPC_STATE__LISTENING;
if (GetLastError() == ERROR_SEM_TIMEOUT)
return IPC_STATE__NOT_LISTENING;
if (GetLastError() == ERROR_FILE_NOT_FOUND)
return IPC_STATE__PATH_NOT_FOUND;
return IPC_STATE__OTHER_ERROR;
}
enum ipc_active_state ipc_get_active_state(const char *path)
{
wchar_t pipe_path[MAX_PATH];
if (initialize_pipe_name(path, pipe_path, ARRAY_SIZE(pipe_path)) < 0)
return IPC_STATE__INVALID_PATH;
return get_active_state(pipe_path);
}
#define WAIT_STEP_MS (50)
static enum ipc_active_state connect_to_server(
const wchar_t *wpath,
DWORD timeout_ms,
const struct ipc_client_connect_options *options,
int *pfd)
{
DWORD t_start_ms, t_waited_ms;
DWORD step_ms;
HANDLE hPipe = INVALID_HANDLE_VALUE;
DWORD mode = PIPE_READMODE_BYTE;
DWORD gle;
*pfd = -1;
for (;;) {
hPipe = CreateFileW(wpath, GENERIC_READ | GENERIC_WRITE,
0, NULL, OPEN_EXISTING, 0, NULL);
if (hPipe != INVALID_HANDLE_VALUE)
break;
gle = GetLastError();
switch (gle) {
case ERROR_FILE_NOT_FOUND:
if (!options->wait_if_not_found)
return IPC_STATE__PATH_NOT_FOUND;
if (!timeout_ms)
return IPC_STATE__PATH_NOT_FOUND;
step_ms = (timeout_ms < WAIT_STEP_MS) ?
timeout_ms : WAIT_STEP_MS;
sleep_millisec(step_ms);
timeout_ms -= step_ms;
break; /* try again */
case ERROR_PIPE_BUSY:
if (!options->wait_if_busy)
return IPC_STATE__NOT_LISTENING;
if (!timeout_ms)
return IPC_STATE__NOT_LISTENING;
t_start_ms = (DWORD)(getnanotime() / 1000000);
if (!WaitNamedPipeW(wpath, timeout_ms)) {
if (GetLastError() == ERROR_SEM_TIMEOUT)
return IPC_STATE__NOT_LISTENING;
return IPC_STATE__OTHER_ERROR;
}
/*
* A pipe server instance became available.
* Race other client processes to connect to
* it.
*
* But first decrement our overall timeout so
* that we don't starve if we keep losing the
* race. But also guard against special
* NPMWAIT_ values (0 and -1).
*/
t_waited_ms = (DWORD)(getnanotime() / 1000000) - t_start_ms;
if (t_waited_ms < timeout_ms)
timeout_ms -= t_waited_ms;
else
timeout_ms = 1;
break; /* try again */
default:
return IPC_STATE__OTHER_ERROR;
}
}
if (!SetNamedPipeHandleState(hPipe, &mode, NULL, NULL)) {
CloseHandle(hPipe);
return IPC_STATE__OTHER_ERROR;
}
*pfd = _open_osfhandle((intptr_t)hPipe, O_RDWR|O_BINARY);
if (*pfd < 0) {
CloseHandle(hPipe);
return IPC_STATE__OTHER_ERROR;
}
/* fd now owns hPipe */
return IPC_STATE__LISTENING;
}
/*
* The default connection timeout for Windows clients.
*
* This is not currently part of the ipc_ API (nor the config settings)
* because of differences between Windows and other platforms.
*
* This value was chosen at random.
*/
#define WINDOWS_CONNECTION_TIMEOUT_MS (30000)
enum ipc_active_state ipc_client_try_connect(
const char *path,
const struct ipc_client_connect_options *options,
struct ipc_client_connection **p_connection)
{
wchar_t wpath[MAX_PATH];
enum ipc_active_state state = IPC_STATE__OTHER_ERROR;
int fd = -1;
*p_connection = NULL;
trace2_region_enter("ipc-client", "try-connect", NULL);
trace2_data_string("ipc-client", NULL, "try-connect/path", path);
if (initialize_pipe_name(path, wpath, ARRAY_SIZE(wpath)) < 0)
state = IPC_STATE__INVALID_PATH;
else
state = connect_to_server(wpath, WINDOWS_CONNECTION_TIMEOUT_MS,
options, &fd);
trace2_data_intmax("ipc-client", NULL, "try-connect/state",
(intmax_t)state);
trace2_region_leave("ipc-client", "try-connect", NULL);
if (state == IPC_STATE__LISTENING) {
(*p_connection) = xcalloc(1, sizeof(struct ipc_client_connection));
(*p_connection)->fd = fd;
}
return state;
}
void ipc_client_close_connection(struct ipc_client_connection *connection)
{
if (!connection)
return;
if (connection->fd != -1)
close(connection->fd);
free(connection);
}
int ipc_client_send_command_to_connection(
struct ipc_client_connection *connection,
const char *message, struct strbuf *answer)
{
int ret = 0;
strbuf_setlen(answer, 0);
trace2_region_enter("ipc-client", "send-command", NULL);
if (write_packetized_from_buf_no_flush(message, strlen(message),
connection->fd) < 0 ||
packet_flush_gently(connection->fd) < 0) {
ret = error(_("could not send IPC command"));
goto done;
}
FlushFileBuffers((HANDLE)_get_osfhandle(connection->fd));
if (read_packetized_to_strbuf(
connection->fd, answer,
PACKET_READ_GENTLE_ON_EOF | PACKET_READ_GENTLE_ON_READ_ERROR) < 0) {
ret = error(_("could not read IPC response"));
goto done;
}
done:
trace2_region_leave("ipc-client", "send-command", NULL);
return ret;
}
int ipc_client_send_command(const char *path,
const struct ipc_client_connect_options *options,
const char *message, struct strbuf *response)
{
int ret = -1;
enum ipc_active_state state;
struct ipc_client_connection *connection = NULL;
state = ipc_client_try_connect(path, options, &connection);
if (state != IPC_STATE__LISTENING)
return ret;
ret = ipc_client_send_command_to_connection(connection, message, response);
ipc_client_close_connection(connection);
return ret;
}
/*
* Duplicate the given pipe handle and wrap it in a file descriptor so
* that we can use pkt-line on it.
*/
static int dup_fd_from_pipe(const HANDLE pipe)
{
HANDLE process = GetCurrentProcess();
HANDLE handle;
int fd;
if (!DuplicateHandle(process, pipe, process, &handle, 0, FALSE,
DUPLICATE_SAME_ACCESS)) {
errno = err_win_to_posix(GetLastError());
return -1;
}
fd = _open_osfhandle((intptr_t)handle, O_RDWR|O_BINARY);
if (fd < 0) {
errno = err_win_to_posix(GetLastError());
CloseHandle(handle);
return -1;
}
/*
* `handle` is now owned by `fd` and will be automatically closed
* when the descriptor is closed.
*/
return fd;
}
/*
* Magic numbers used to annotate callback instance data.
* These are used to help guard against accidentally passing the
* wrong instance data across multiple levels of callbacks (which
* is easy to do if there are `void*` arguments).
*/
enum magic {
MAGIC_SERVER_REPLY_DATA,
MAGIC_SERVER_THREAD_DATA,
MAGIC_SERVER_DATA,
};
struct ipc_server_reply_data {
enum magic magic;
int fd;
struct ipc_server_thread_data *server_thread_data;
};
struct ipc_server_thread_data {
enum magic magic;
struct ipc_server_thread_data *next_thread;
struct ipc_server_data *server_data;
pthread_t pthread_id;
HANDLE hPipe;
};
/*
* On Windows, the conceptual "ipc-server" is implemented as a pool of
* n idential/peer "server-thread" threads. That is, there is no
* hierarchy of threads; and therefore no controller thread managing
* the pool. Each thread has an independent handle to the named pipe,
* receives incoming connections, processes the client, and re-uses
* the pipe for the next client connection.
*
* Therefore, the "ipc-server" only needs to maintain a list of the
* spawned threads for eventual "join" purposes.
*
* A single "stop-event" is visible to all of the server threads to
* tell them to shutdown (when idle).
*/
struct ipc_server_data {
enum magic magic;
ipc_server_application_cb *application_cb;
void *application_data;
struct strbuf buf_path;
wchar_t wpath[MAX_PATH];
HANDLE hEventStopRequested;
struct ipc_server_thread_data *thread_list;
int is_stopped;
};
enum connect_result {
CR_CONNECTED = 0,
CR_CONNECT_PENDING,
CR_CONNECT_ERROR,
CR_WAIT_ERROR,
CR_SHUTDOWN,
};
static enum connect_result queue_overlapped_connect(
struct ipc_server_thread_data *server_thread_data,
OVERLAPPED *lpo)
{
if (ConnectNamedPipe(server_thread_data->hPipe, lpo))
goto failed;
switch (GetLastError()) {
case ERROR_IO_PENDING:
return CR_CONNECT_PENDING;
case ERROR_PIPE_CONNECTED:
SetEvent(lpo->hEvent);
return CR_CONNECTED;
default:
break;
}
failed:
error(_("ConnectNamedPipe failed for '%s' (%lu)"),
server_thread_data->server_data->buf_path.buf,
GetLastError());
return CR_CONNECT_ERROR;
}
/*
* Use Windows Overlapped IO to wait for a connection or for our event
* to be signalled.
*/
static enum connect_result wait_for_connection(
struct ipc_server_thread_data *server_thread_data,
OVERLAPPED *lpo)
{
enum connect_result r;
HANDLE waitHandles[2];
DWORD dwWaitResult;
r = queue_overlapped_connect(server_thread_data, lpo);
if (r != CR_CONNECT_PENDING)
return r;
waitHandles[0] = server_thread_data->server_data->hEventStopRequested;
waitHandles[1] = lpo->hEvent;
dwWaitResult = WaitForMultipleObjects(2, waitHandles, FALSE, INFINITE);
switch (dwWaitResult) {
case WAIT_OBJECT_0 + 0:
return CR_SHUTDOWN;
case WAIT_OBJECT_0 + 1:
ResetEvent(lpo->hEvent);
return CR_CONNECTED;
default:
return CR_WAIT_ERROR;
}
}
/*
* Forward declare our reply callback function so that any compiler
* errors are reported when we actually define the function (in addition
* to any errors reported when we try to pass this callback function as
* a parameter in a function call). The former are easier to understand.
*/
static ipc_server_reply_cb do_io_reply_callback;
/*
* Relay application's response message to the client process.
* (We do not flush at this point because we allow the caller
* to chunk data to the client thru us.)
*/
static int do_io_reply_callback(struct ipc_server_reply_data *reply_data,
const char *response, size_t response_len)
{
if (reply_data->magic != MAGIC_SERVER_REPLY_DATA)
BUG("reply_cb called with wrong instance data");
return write_packetized_from_buf_no_flush(response, response_len,
reply_data->fd);
}
/*
* Receive the request/command from the client and pass it to the
* registered request-callback. The request-callback will compose
* a response and call our reply-callback to send it to the client.
*
* Simple-IPC only contains one round trip, so we flush and close
* here after the response.
*/
static int do_io(struct ipc_server_thread_data *server_thread_data)
{
struct strbuf buf = STRBUF_INIT;
struct ipc_server_reply_data reply_data;
int ret = 0;
reply_data.magic = MAGIC_SERVER_REPLY_DATA;
reply_data.server_thread_data = server_thread_data;
reply_data.fd = dup_fd_from_pipe(server_thread_data->hPipe);
if (reply_data.fd < 0)
return error(_("could not create fd from pipe for '%s'"),
server_thread_data->server_data->buf_path.buf);
ret = read_packetized_to_strbuf(
reply_data.fd, &buf,
PACKET_READ_GENTLE_ON_EOF | PACKET_READ_GENTLE_ON_READ_ERROR);
if (ret >= 0) {
ret = server_thread_data->server_data->application_cb(
server_thread_data->server_data->application_data,
buf.buf, do_io_reply_callback, &reply_data);
packet_flush_gently(reply_data.fd);
FlushFileBuffers((HANDLE)_get_osfhandle((reply_data.fd)));
}
else {
/*
* The client probably disconnected/shutdown before it
* could send a well-formed message. Ignore it.
*/
}
strbuf_release(&buf);
close(reply_data.fd);
return ret;
}
/*
* Handle IPC request and response with this connected client. And reset
* the pipe to prepare for the next client.
*/
static int use_connection(struct ipc_server_thread_data *server_thread_data)
{
int ret;
ret = do_io(server_thread_data);
FlushFileBuffers(server_thread_data->hPipe);
DisconnectNamedPipe(server_thread_data->hPipe);
return ret;
}
/*
* Thread proc for an IPC server worker thread. It handles a series of
* connections from clients. It cleans and reuses the hPipe between each
* client.
*/
static void *server_thread_proc(void *_server_thread_data)
{
struct ipc_server_thread_data *server_thread_data = _server_thread_data;
HANDLE hEventConnected = INVALID_HANDLE_VALUE;
OVERLAPPED oConnect;
enum connect_result cr;
int ret;
assert(server_thread_data->hPipe != INVALID_HANDLE_VALUE);
trace2_thread_start("ipc-server");
trace2_data_string("ipc-server", NULL, "pipe",
server_thread_data->server_data->buf_path.buf);
hEventConnected = CreateEventW(NULL, TRUE, FALSE, NULL);
memset(&oConnect, 0, sizeof(oConnect));
oConnect.hEvent = hEventConnected;
for (;;) {
cr = wait_for_connection(server_thread_data, &oConnect);
switch (cr) {
case CR_SHUTDOWN:
goto finished;
case CR_CONNECTED:
ret = use_connection(server_thread_data);
if (ret == SIMPLE_IPC_QUIT) {
ipc_server_stop_async(
server_thread_data->server_data);
goto finished;
}
if (ret > 0) {
/*
* Ignore (transient) IO errors with this
* client and reset for the next client.
*/
}
break;
case CR_CONNECT_PENDING:
/* By construction, this should not happen. */
BUG("ipc-server[%s]: unexpeced CR_CONNECT_PENDING",
server_thread_data->server_data->buf_path.buf);
case CR_CONNECT_ERROR:
case CR_WAIT_ERROR:
/*
* Ignore these theoretical errors.
*/
DisconnectNamedPipe(server_thread_data->hPipe);
break;
default:
BUG("unandled case after wait_for_connection");
}
}
finished:
CloseHandle(server_thread_data->hPipe);
CloseHandle(hEventConnected);
trace2_thread_exit();
return NULL;
}
static HANDLE create_new_pipe(wchar_t *wpath, int is_first)
{
HANDLE hPipe;
DWORD dwOpenMode, dwPipeMode;
LPSECURITY_ATTRIBUTES lpsa = NULL;
dwOpenMode = PIPE_ACCESS_INBOUND | PIPE_ACCESS_OUTBOUND |
FILE_FLAG_OVERLAPPED;
dwPipeMode = PIPE_TYPE_MESSAGE | PIPE_READMODE_BYTE | PIPE_WAIT |
PIPE_REJECT_REMOTE_CLIENTS;
if (is_first) {
dwOpenMode |= FILE_FLAG_FIRST_PIPE_INSTANCE;
/*
* On Windows, the first server pipe instance gets to
* set the ACL / Security Attributes on the named
* pipe; subsequent instances inherit and cannot
* change them.
*
* TODO Should we allow the application layer to
* specify security attributes, such as `LocalService`
* or `LocalSystem`, when we create the named pipe?
* This question is probably not important when the
* daemon is started by a foreground user process and
* only needs to talk to the current user, but may be
* if the daemon is run via the Control Panel as a
* System Service.
*/
}
hPipe = CreateNamedPipeW(wpath, dwOpenMode, dwPipeMode,
PIPE_UNLIMITED_INSTANCES, 1024, 1024, 0, lpsa);
return hPipe;
}
int ipc_server_run_async(struct ipc_server_data **returned_server_data,
const char *path, const struct ipc_server_opts *opts,
ipc_server_application_cb *application_cb,
void *application_data)
{
struct ipc_server_data *server_data;
wchar_t wpath[MAX_PATH];
HANDLE hPipeFirst = INVALID_HANDLE_VALUE;
int k;
int ret = 0;
int nr_threads = opts->nr_threads;
*returned_server_data = NULL;
ret = initialize_pipe_name(path, wpath, ARRAY_SIZE(wpath));
if (ret < 0) {
errno = EINVAL;
return -1;
}
hPipeFirst = create_new_pipe(wpath, 1);
if (hPipeFirst == INVALID_HANDLE_VALUE) {
errno = EADDRINUSE;
return -2;
}
server_data = xcalloc(1, sizeof(*server_data));
server_data->magic = MAGIC_SERVER_DATA;
server_data->application_cb = application_cb;
server_data->application_data = application_data;
server_data->hEventStopRequested = CreateEvent(NULL, TRUE, FALSE, NULL);
strbuf_init(&server_data->buf_path, 0);
strbuf_addstr(&server_data->buf_path, path);
wcscpy(server_data->wpath, wpath);
if (nr_threads < 1)
nr_threads = 1;
for (k = 0; k < nr_threads; k++) {
struct ipc_server_thread_data *std;
std = xcalloc(1, sizeof(*std));
std->magic = MAGIC_SERVER_THREAD_DATA;
std->server_data = server_data;
std->hPipe = INVALID_HANDLE_VALUE;
std->hPipe = (k == 0)
? hPipeFirst
: create_new_pipe(server_data->wpath, 0);
if (std->hPipe == INVALID_HANDLE_VALUE) {
/*
* If we've reached a pipe instance limit for
* this path, just use fewer threads.
*/
free(std);
break;
}
if (pthread_create(&std->pthread_id, NULL,
server_thread_proc, std)) {
/*
* Likewise, if we're out of threads, just use
* fewer threads than requested.
*
* However, we just give up if we can't even get
* one thread. This should not happen.
*/
if (k == 0)
die(_("could not start thread[0] for '%s'"),
path);
CloseHandle(std->hPipe);
free(std);
break;
}
std->next_thread = server_data->thread_list;
server_data->thread_list = std;
}
*returned_server_data = server_data;
return 0;
}
int ipc_server_stop_async(struct ipc_server_data *server_data)
{
if (!server_data)
return 0;
/*
* Gently tell all of the ipc_server threads to shutdown.
* This will be seen the next time they are idle (and waiting
* for a connection).
*
* We DO NOT attempt to force them to drop an active connection.
*/
SetEvent(server_data->hEventStopRequested);
return 0;
}
int ipc_server_await(struct ipc_server_data *server_data)
{
DWORD dwWaitResult;
if (!server_data)
return 0;
dwWaitResult = WaitForSingleObject(server_data->hEventStopRequested, INFINITE);
if (dwWaitResult != WAIT_OBJECT_0)
return error(_("wait for hEvent failed for '%s'"),
server_data->buf_path.buf);
while (server_data->thread_list) {
struct ipc_server_thread_data *std = server_data->thread_list;
pthread_join(std->pthread_id, NULL);
server_data->thread_list = std->next_thread;
free(std);
}
server_data->is_stopped = 1;
return 0;
}
void ipc_server_free(struct ipc_server_data *server_data)
{
if (!server_data)
return;
if (!server_data->is_stopped)
BUG("cannot free ipc-server while running for '%s'",
server_data->buf_path.buf);
strbuf_release(&server_data->buf_path);
if (server_data->hEventStopRequested != INVALID_HANDLE_VALUE)
CloseHandle(server_data->hEventStopRequested);
while (server_data->thread_list) {
struct ipc_server_thread_data *std = server_data->thread_list;
server_data->thread_list = std->next_thread;
free(std);
}
free(server_data);
}

View File

@@ -2515,9 +2515,14 @@ int git_config_get_max_percent_split_change(void)
return -1; /* default value */
}
int git_config_get_fsmonitor(void)
int repo_config_get_fsmonitor(struct repository *r)
{
if (git_config_get_pathname("core.fsmonitor", &core_fsmonitor))
if (r->settings.use_builtin_fsmonitor > 0) {
core_fsmonitor = "(built-in daemon)";
return 1;
}
if (repo_config_get_pathname(r, "core.fsmonitor", &core_fsmonitor))
core_fsmonitor = getenv("GIT_TEST_FSMONITOR");
if (core_fsmonitor && !*core_fsmonitor)

View File

@@ -607,7 +607,7 @@ int git_config_get_index_threads(int *dest);
int git_config_get_untracked_cache(void);
int git_config_get_split_index(void);
int git_config_get_max_percent_split_change(void);
int git_config_get_fsmonitor(void);
int repo_config_get_fsmonitor(struct repository *r);
/* This dies if the configured or default date is in the future */
int git_config_get_expiry(const char *key, const char **output);

View File

@@ -147,6 +147,8 @@ ifeq ($(uname_S),Darwin)
MSGFMT = /usr/local/opt/gettext/bin/msgfmt
endif
endif
FSMONITOR_DAEMON_BACKEND = macos
BASIC_LDFLAGS += -framework CoreServices
endif
ifeq ($(uname_S),SunOS)
NEEDS_SOCKET = YesPlease
@@ -420,10 +422,12 @@ ifeq ($(uname_S),Windows)
# so we don't need this:
#
# SNPRINTF_RETURNS_BOGUS = YesPlease
FSMONITOR_DAEMON_BACKEND = win32
NO_SVN_TESTS = YesPlease
RUNTIME_PREFIX = YesPlease
HAVE_WPGMPTR = YesWeDo
NO_ST_BLOCKS_IN_STRUCT_STAT = YesPlease
USE_WIN32_IPC = YesPlease
USE_WIN32_MMAP = YesPlease
MMAP_PREVENTS_DELETE = UnfortunatelyYes
# USE_NED_ALLOCATOR = YesPlease
@@ -604,9 +608,11 @@ ifneq (,$(findstring MINGW,$(uname_S)))
NO_STRTOUMAX = YesPlease
NO_MKDTEMP = YesPlease
NO_SVN_TESTS = YesPlease
FSMONITOR_DAEMON_BACKEND = win32
RUNTIME_PREFIX = YesPlease
HAVE_WPGMPTR = YesWeDo
NO_ST_BLOCKS_IN_STRUCT_STAT = YesPlease
USE_WIN32_IPC = YesPlease
USE_WIN32_MMAP = YesPlease
MMAP_PREVENTS_DELETE = UnfortunatelyYes
USE_NED_ALLOCATOR = YesPlease

View File

@@ -250,7 +250,21 @@ if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux")
add_compile_definitions(PROCFS_EXECUTABLE_PATH="/proc/self/exe" HAVE_DEV_TTY )
list(APPEND compat_SOURCES unix-socket.c)
list(APPEND compat_SOURCES unix-socket.c unix-stream-server.c)
endif()
if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
list(APPEND compat_SOURCES compat/simple-ipc/ipc-shared.c compat/simple-ipc/ipc-win32.c)
else()
list(APPEND compat_SOURCES compat/simple-ipc/ipc-shared.c compat/simple-ipc/ipc-unix-socket.c)
endif()
if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
add_compile_definitions(HAVE_FSMONITOR_DAEMON_BACKEND)
list(APPEND compat_SOURCES compat/fsmonitor/fsmonitor-fs-listen-win32.c)
elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
add_compile_definitions(HAVE_FSMONITOR_DAEMON_BACKEND)
list(APPEND compat_SOURCES compat/fsmonitor/fsmonitor-fs-listen-macos.c)
endif()
set(EXE_EXTENSION ${CMAKE_EXECUTABLE_SUFFIX})

View File

@@ -884,9 +884,13 @@ static int apply_multi_file_filter(const char *path, const char *src, size_t len
goto done;
if (fd >= 0)
err = write_packetized_from_fd(fd, process->in);
err = write_packetized_from_fd_no_flush(fd, process->in);
else
err = write_packetized_from_buf(src, len, process->in);
err = write_packetized_from_buf_no_flush(src, len, process->in);
if (err)
goto done;
err = packet_flush_gently(process->in);
if (err)
goto done;
@@ -903,7 +907,8 @@ static int apply_multi_file_filter(const char *path, const char *src, size_t len
if (err)
goto done;
err = read_packetized_to_strbuf(process->out, &nbuf) < 0;
err = read_packetized_to_strbuf(process->out, &nbuf,
PACKET_READ_GENTLE_ON_EOF) < 0;
if (err)
goto done;

142
fsmonitor--daemon.h Normal file
View File

@@ -0,0 +1,142 @@
#ifndef FSMONITOR_DAEMON_H
#define FSMONITOR_DAEMON_H
#ifdef HAVE_FSMONITOR_DAEMON_BACKEND
#include "cache.h"
#include "dir.h"
#include "run-command.h"
#include "simple-ipc.h"
#include "thread-utils.h"
struct fsmonitor_batch;
struct fsmonitor_token_data;
/*
* Create a new batch of path(s). The returned batch is considered
* private and not linked into the fsmonitor daemon state. The caller
* should fill this batch with one or more paths and then publish it.
*/
struct fsmonitor_batch *fsmonitor_batch__new(void);
/*
* Free this batch and return the value of the batch->next field.
*/
struct fsmonitor_batch *fsmonitor_batch__free(struct fsmonitor_batch *batch);
/*
* Add this path to this batch of modified files.
*
* The batch should be private and NOT (yet) linked into the fsmonitor
* daemon state and therefore not yet visible to worker threads and so
* no locking is required.
*/
void fsmonitor_batch__add_path(struct fsmonitor_batch *batch, const char *path);
struct fsmonitor_daemon_backend_data; /* opaque platform-specific data */
struct fsmonitor_daemon_state {
pthread_t listener_thread;
pthread_mutex_t main_lock;
struct strbuf path_worktree_watch;
struct strbuf path_gitdir_watch;
int nr_paths_watching;
struct fsmonitor_token_data *current_token_data;
struct strbuf path_cookie_prefix;
pthread_cond_t cookies_cond;
int cookie_seq;
struct hashmap cookies;
int error_code;
struct fsmonitor_daemon_backend_data *backend_data;
struct ipc_server_data *ipc_server_data;
int test_client_delay_ms;
};
/*
* Pathname classifications.
*
* The daemon classifies the pathnames that it receives from file
* system notification events into the following categories and uses
* that to decide whether clients are told about them. (And to watch
* for file system synchronization events.)
*
* The client should only care about paths within the working
* directory proper (inside the working directory and not ".git" nor
* inside of ".git/"). That is, the client has read the index and is
* asking for a list of any paths in the working directory that have
* been modified since the last token. The client does not care about
* file system changes within the .git directory (such as new loose
* objects or packfiles). So the client will only receive paths that
* are classified as IS_WORKDIR_PATH.
*
* The daemon uses the IS_DOT_GIT and IS_GITDIR internally to mean the
* exact ".git" directory or GITDIR. If the daemon receives a delete
* event for either of these directories, it will automatically
* shutdown, for example.
*
* Note that the daemon DOES NOT explicitly watch nor special case the
* ".git/index" file. The daemon does not read the index and does not
* have any internal index-relative state. The daemon only collects
* the set of modified paths within the working directory.
*/
enum fsmonitor_path_type {
IS_WORKDIR_PATH = 0,
IS_DOT_GIT,
IS_INSIDE_DOT_GIT,
IS_INSIDE_DOT_GIT_WITH_COOKIE_PREFIX,
IS_GITDIR,
IS_INSIDE_GITDIR,
IS_INSIDE_GITDIR_WITH_COOKIE_PREFIX,
IS_OUTSIDE_CONE,
};
/*
* Classify a pathname relative to the root of the working directory.
*/
enum fsmonitor_path_type fsmonitor_classify_path_workdir_relative(
const char *relative_path);
/*
* Classify a pathname relative to a <gitdir> that is external to the
* worktree directory.
*/
enum fsmonitor_path_type fsmonitor_classify_path_gitdir_relative(
const char *relative_path);
/*
* Classify an absolute pathname received from a filesystem event.
*/
enum fsmonitor_path_type fsmonitor_classify_path_absolute(
struct fsmonitor_daemon_state *state,
const char *path);
/*
* Prepend the this batch of path(s) onto the list of batches associated
* with the current token. This makes the batch visible to worker threads.
*
* The caller no longer owns the batch and must not free it.
*
* Wake up the client threads waiting on these cookies.
*/
void fsmonitor_publish(struct fsmonitor_daemon_state *state,
struct fsmonitor_batch *batch,
const struct string_list *cookie_names);
/*
* If the platform-specific layer loses sync with the filesystem,
* it should call this to invalidate cached data and abort waiting
* threads.
*/
void fsmonitor_force_resync(struct fsmonitor_daemon_state *state);
#endif /* HAVE_FSMONITOR_DAEMON_BACKEND */
#endif /* FSMONITOR_DAEMON_H */

153
fsmonitor-ipc.c Normal file
View File

@@ -0,0 +1,153 @@
#include "cache.h"
#include "fsmonitor.h"
#include "fsmonitor-ipc.h"
#include "run-command.h"
#include "strbuf.h"
#include "trace2.h"
#ifdef HAVE_FSMONITOR_DAEMON_BACKEND
#define FSMONITOR_DAEMON_IS_SUPPORTED 1
#else
#define FSMONITOR_DAEMON_IS_SUPPORTED 0
#endif
/*
* A trivial function so that this source file always defines at least
* one symbol even when the feature is not supported. This quiets an
* annoying compiler error.
*/
int fsmonitor_ipc__is_supported(void)
{
return FSMONITOR_DAEMON_IS_SUPPORTED;
}
#ifdef HAVE_FSMONITOR_DAEMON_BACKEND
GIT_PATH_FUNC(fsmonitor_ipc__get_path, "fsmonitor")
enum ipc_active_state fsmonitor_ipc__get_state(void)
{
return ipc_get_active_state(fsmonitor_ipc__get_path());
}
static int spawn_daemon(void)
{
const char *args[] = { "fsmonitor--daemon", "--start", NULL };
return run_command_v_opt_tr2(args, RUN_COMMAND_NO_STDIN | RUN_GIT_CMD,
"fsmonitor");
}
int fsmonitor_ipc__send_query(const char *since_token,
struct strbuf *answer)
{
int ret = -1;
int tried_to_spawn = 0;
enum ipc_active_state state = IPC_STATE__OTHER_ERROR;
struct ipc_client_connection *connection = NULL;
struct ipc_client_connect_options options
= IPC_CLIENT_CONNECT_OPTIONS_INIT;
options.wait_if_busy = 1;
options.wait_if_not_found = 0;
trace2_region_enter("fsm_client", "query", NULL);
trace2_data_string("fsm_client", NULL, "query/command",
since_token);
try_again:
state = ipc_client_try_connect(fsmonitor_ipc__get_path(), &options,
&connection);
switch (state) {
case IPC_STATE__LISTENING:
ret = ipc_client_send_command_to_connection(
connection, since_token, answer);
ipc_client_close_connection(connection);
trace2_data_intmax("fsm_client", NULL,
"query/response-length", answer->len);
if (fsmonitor_is_trivial_response(answer))
trace2_data_intmax("fsm_client", NULL,
"query/trivial-response", 1);
goto done;
case IPC_STATE__NOT_LISTENING:
ret = error(_("fsmonitor_ipc__send_query: daemon not available"));
goto done;
case IPC_STATE__PATH_NOT_FOUND:
if (tried_to_spawn)
goto done;
tried_to_spawn++;
if (spawn_daemon())
goto done;
/*
* Try again, but this time give the daemon a chance to
* actually create the pipe/socket.
*
* Granted, the daemon just started so it can't possibly have
* any FS cached yet, so we'll always get a trivial answer.
* BUT the answer should include a new token that can serve
* as the basis for subsequent requests.
*/
options.wait_if_not_found = 1;
goto try_again;
case IPC_STATE__INVALID_PATH:
ret = error(_("fsmonitor_ipc__send_query: invalid path '%s'"),
fsmonitor_ipc__get_path());
goto done;
case IPC_STATE__OTHER_ERROR:
default:
ret = error(_("fsmonitor_ipc__send_query: unspecified error on '%s'"),
fsmonitor_ipc__get_path());
goto done;
}
done:
trace2_region_leave("fsm_client", "query", NULL);
return ret;
}
int fsmonitor_ipc__send_command(const char *command,
struct strbuf *answer)
{
struct ipc_client_connection *connection = NULL;
struct ipc_client_connect_options options
= IPC_CLIENT_CONNECT_OPTIONS_INIT;
int ret;
enum ipc_active_state state;
strbuf_reset(answer);
options.wait_if_busy = 1;
options.wait_if_not_found = 0;
state = ipc_client_try_connect(fsmonitor_ipc__get_path(), &options,
&connection);
if (state != IPC_STATE__LISTENING) {
die("fsmonitor--daemon is not running");
return -1;
}
ret = ipc_client_send_command_to_connection(connection, command, answer);
ipc_client_close_connection(connection);
if (ret == -1) {
die("could not send '%s' command to fsmonitor--daemon",
command);
return -1;
}
return 0;
}
#endif

48
fsmonitor-ipc.h Normal file
View File

@@ -0,0 +1,48 @@
#ifndef FSMONITOR_IPC_H
#define FSMONITOR_IPC_H
/*
* Returns true if a filesystem notification backend is defined
* for this platform. This symbol must always be visible and
* outside of the HAVE_ ifdef.
*/
int fsmonitor_ipc__is_supported(void);
#ifdef HAVE_FSMONITOR_DAEMON_BACKEND
#include "run-command.h"
#include "simple-ipc.h"
/*
* Returns the pathname to the IPC named pipe or Unix domain socket
* where a `git-fsmonitor--daemon` process will listen. This is a
* per-worktree value.
*/
const char *fsmonitor_ipc__get_path(void);
/*
* Try to determine whether there is a `git-fsmonitor--daemon` process
* listening on the IPC pipe/socket.
*/
enum ipc_active_state fsmonitor_ipc__get_state(void);
/*
* Connect to a `git-fsmonitor--daemon` process via simple-ipc
* and ask for the set of changed files since the given token.
*
* This DOES NOT use the hook interface.
*
* Spawn a daemon process in the background if necessary.
*/
int fsmonitor_ipc__send_query(const char *since_token,
struct strbuf *answer);
/*
* Connect to a `git-fsmonitor--daemon` process via simple-ipc and
* send a command verb. If no daemon is available, we DO NOT try to
* start one.
*/
int fsmonitor_ipc__send_command(const char *command,
struct strbuf *answer);
#endif /* HAVE_FSMONITOR_DAEMON_BACKEND */
#endif /* FSMONITOR_IPC_H */

View File

@@ -3,6 +3,7 @@
#include "dir.h"
#include "ewah/ewok.h"
#include "fsmonitor.h"
#include "fsmonitor-ipc.h"
#include "run-command.h"
#include "strbuf.h"
@@ -148,14 +149,27 @@ void write_fsmonitor_extension(struct strbuf *sb, struct index_state *istate)
/*
* Call the query-fsmonitor hook passing the last update token of the saved results.
*/
static int query_fsmonitor(int version, const char *last_update, struct strbuf *query_result)
static int query_fsmonitor(int version, struct index_state *istate, struct strbuf *query_result)
{
struct repository *r = istate->repo ? istate->repo : the_repository;
const char *last_update = istate->fsmonitor_last_update;
struct child_process cp = CHILD_PROCESS_INIT;
int result;
if (!core_fsmonitor)
return -1;
if (r->settings.use_builtin_fsmonitor > 0) {
#ifdef HAVE_FSMONITOR_DAEMON_BACKEND
return fsmonitor_ipc__send_query(last_update, query_result);
#else
/* Fake a trivial response. */
warning(_("fsmonitor--daemon unavailable; falling back"));
strbuf_add(query_result, "/", 2);
return 0;
#endif
}
strvec_push(&cp.args, core_fsmonitor);
strvec_pushf(&cp.args, "%d", version);
strvec_pushf(&cp.args, "%s", last_update);
@@ -263,7 +277,7 @@ void refresh_fsmonitor(struct index_state *istate)
if (istate->fsmonitor_last_update) {
if (hook_version == -1 || hook_version == HOOK_INTERFACE_VERSION2) {
query_success = !query_fsmonitor(HOOK_INTERFACE_VERSION2,
istate->fsmonitor_last_update, &query_result);
istate, &query_result);
if (query_success) {
if (hook_version < 0)
@@ -293,7 +307,7 @@ void refresh_fsmonitor(struct index_state *istate)
if (hook_version == HOOK_INTERFACE_VERSION1) {
query_success = !query_fsmonitor(HOOK_INTERFACE_VERSION1,
istate->fsmonitor_last_update, &query_result);
istate, &query_result);
}
trace_performance_since(last_update, "fsmonitor process '%s'", core_fsmonitor);
@@ -339,6 +353,16 @@ void refresh_fsmonitor(struct index_state *istate)
}
strbuf_release(&query_result);
/*
* If the fsmonitor response and the subsequent scan of the disk
* did not cause the in-memory index to be marked dirty, then force
* it so that we advance the fsmonitor token in our extension, so
* that future requests don't keep re-requesting the same range.
*/
if (istate->fsmonitor_last_update &&
strcmp(istate->fsmonitor_last_update, last_update_token.buf))
istate->cache_changed |= FSMONITOR_CHANGED;
/* Now that we've updated istate, save the last_update_token */
FREE_AND_NULL(istate->fsmonitor_last_update);
istate->fsmonitor_last_update = strbuf_detach(&last_update_token, NULL);
@@ -411,7 +435,7 @@ void remove_fsmonitor(struct index_state *istate)
void tweak_fsmonitor(struct index_state *istate)
{
unsigned int i;
int fsmonitor_enabled = git_config_get_fsmonitor();
int fsmonitor_enabled = repo_config_get_fsmonitor(istate->repo ? istate->repo : the_repository);
if (istate->fsmonitor_dirty) {
if (fsmonitor_enabled) {

1
git.c
View File

@@ -523,6 +523,7 @@ static struct cmd_struct commands[] = {
{ "format-patch", cmd_format_patch, RUN_SETUP },
{ "fsck", cmd_fsck, RUN_SETUP },
{ "fsck-objects", cmd_fsck, RUN_SETUP },
{ "fsmonitor--daemon", cmd_fsmonitor__daemon, RUN_SETUP },
{ "gc", cmd_gc, RUN_SETUP },
{ "get-tar-commit-id", cmd_get_tar_commit_id, NO_PARSEOPT },
{ "grep", cmd_grep, RUN_SETUP_GENTLY },

4
help.c
View File

@@ -11,6 +11,7 @@
#include "version.h"
#include "refs.h"
#include "parse-options.h"
#include "fsmonitor-ipc.h"
struct category_description {
uint32_t category;
@@ -664,6 +665,9 @@ void get_version_info(struct strbuf *buf, int show_build_options)
strbuf_addf(buf, "sizeof-size_t: %d\n", (int)sizeof(size_t));
strbuf_addf(buf, "shell-path: %s\n", SHELL_PATH);
/* NEEDSWORK: also save and output GIT-BUILD_OPTIONS? */
if (fsmonitor_ipc__is_supported())
strbuf_addstr(buf, "feature: fsmonitor--daemon\n");
}
}

View File

@@ -196,17 +196,25 @@ int packet_write_fmt_gently(int fd, const char *fmt, ...)
static int packet_write_gently(const int fd_out, const char *buf, size_t size)
{
static char packet_write_buffer[LARGE_PACKET_MAX];
char header[4];
size_t packet_size;
if (size > sizeof(packet_write_buffer) - 4)
if (size > LARGE_PACKET_DATA_MAX)
return error(_("packet write failed - data exceeds max packet size"));
packet_trace(buf, size, 1);
packet_size = size + 4;
set_packet_header(packet_write_buffer, packet_size);
memcpy(packet_write_buffer + 4, buf, size);
if (write_in_full(fd_out, packet_write_buffer, packet_size) < 0)
set_packet_header(header, packet_size);
/*
* Write the header and the buffer in 2 parts so that we do not need
* to allocate a buffer or rely on a static buffer. This avoids perf
* and multi-threading issues.
*/
if (write_in_full(fd_out, header, 4) < 0 ||
write_in_full(fd_out, buf, size) < 0)
return error(_("packet write failed"));
return 0;
}
@@ -242,26 +250,27 @@ void packet_buf_write_len(struct strbuf *buf, const char *data, size_t len)
packet_trace(data, len, 1);
}
int write_packetized_from_fd(int fd_in, int fd_out)
int write_packetized_from_fd_no_flush(int fd_in, int fd_out)
{
static char buf[LARGE_PACKET_DATA_MAX];
char *buf = xmalloc(LARGE_PACKET_DATA_MAX);
int err = 0;
ssize_t bytes_to_write;
while (!err) {
bytes_to_write = xread(fd_in, buf, sizeof(buf));
if (bytes_to_write < 0)
bytes_to_write = xread(fd_in, buf, LARGE_PACKET_DATA_MAX);
if (bytes_to_write < 0) {
free(buf);
return COPY_READ_ERROR;
}
if (bytes_to_write == 0)
break;
err = packet_write_gently(fd_out, buf, bytes_to_write);
}
if (!err)
err = packet_flush_gently(fd_out);
free(buf);
return err;
}
int write_packetized_from_buf(const char *src_in, size_t len, int fd_out)
int write_packetized_from_buf_no_flush(const char *src_in, size_t len, int fd_out)
{
int err = 0;
size_t bytes_written = 0;
@@ -277,8 +286,6 @@ int write_packetized_from_buf(const char *src_in, size_t len, int fd_out)
err = packet_write_gently(fd_out, src_in + bytes_written, bytes_to_write);
bytes_written += bytes_to_write;
}
if (!err)
err = packet_flush_gently(fd_out);
return err;
}
@@ -298,8 +305,11 @@ static int get_packet_data(int fd, char **src_buf, size_t *src_size,
*src_size -= ret;
} else {
ret = read_in_full(fd, dst, size);
if (ret < 0)
if (ret < 0) {
if (options & PACKET_READ_GENTLE_ON_READ_ERROR)
return error_errno(_("read error"));
die_errno(_("read error"));
}
}
/* And complain if we didn't get enough bytes to satisfy the read. */
@@ -307,6 +317,8 @@ static int get_packet_data(int fd, char **src_buf, size_t *src_size,
if (options & PACKET_READ_GENTLE_ON_EOF)
return -1;
if (options & PACKET_READ_GENTLE_ON_READ_ERROR)
return error(_("the remote end hung up unexpectedly"));
die(_("the remote end hung up unexpectedly"));
}
@@ -335,6 +347,9 @@ enum packet_read_status packet_read_with_status(int fd, char **src_buffer,
len = packet_length(linelen);
if (len < 0) {
if (options & PACKET_READ_GENTLE_ON_READ_ERROR)
return error(_("protocol error: bad line length "
"character: %.4s"), linelen);
die(_("protocol error: bad line length character: %.4s"), linelen);
} else if (!len) {
packet_trace("0000", 4, 0);
@@ -349,12 +364,19 @@ enum packet_read_status packet_read_with_status(int fd, char **src_buffer,
*pktlen = 0;
return PACKET_READ_RESPONSE_END;
} else if (len < 4) {
if (options & PACKET_READ_GENTLE_ON_READ_ERROR)
return error(_("protocol error: bad line length %d"),
len);
die(_("protocol error: bad line length %d"), len);
}
len -= 4;
if ((unsigned)len >= size)
if ((unsigned)len >= size) {
if (options & PACKET_READ_GENTLE_ON_READ_ERROR)
return error(_("protocol error: bad line length %d"),
len);
die(_("protocol error: bad line length %d"), len);
}
if (get_packet_data(fd, src_buffer, src_len, buffer, len, options) < 0) {
*pktlen = -1;
@@ -421,7 +443,7 @@ char *packet_read_line_buf(char **src, size_t *src_len, int *dst_len)
return packet_read_line_generic(-1, src, src_len, dst_len);
}
ssize_t read_packetized_to_strbuf(int fd_in, struct strbuf *sb_out)
ssize_t read_packetized_to_strbuf(int fd_in, struct strbuf *sb_out, int options)
{
int packet_len;
@@ -437,7 +459,7 @@ ssize_t read_packetized_to_strbuf(int fd_in, struct strbuf *sb_out)
* that there is already room for the extra byte.
*/
sb_out->buf + sb_out->len, LARGE_PACKET_DATA_MAX+1,
PACKET_READ_GENTLE_ON_EOF);
options);
if (packet_len <= 0)
break;
sb_out->len += packet_len;

View File

@@ -32,8 +32,8 @@ void packet_buf_write(struct strbuf *buf, const char *fmt, ...) __attribute__((f
void packet_buf_write_len(struct strbuf *buf, const char *data, size_t len);
int packet_flush_gently(int fd);
int packet_write_fmt_gently(int fd, const char *fmt, ...) __attribute__((format (printf, 2, 3)));
int write_packetized_from_fd(int fd_in, int fd_out);
int write_packetized_from_buf(const char *src_in, size_t len, int fd_out);
int write_packetized_from_fd_no_flush(int fd_in, int fd_out);
int write_packetized_from_buf_no_flush(const char *src_in, size_t len, int fd_out);
/*
* Read a packetized line into the buffer, which must be at least size bytes
@@ -68,10 +68,15 @@ int write_packetized_from_buf(const char *src_in, size_t len, int fd_out);
*
* If options contains PACKET_READ_DIE_ON_ERR_PACKET, it dies when it sees an
* ERR packet.
*
* If options contains PACKET_READ_GENTLE_ON_READ_ERROR, we will not die
* on read errors, but instead return -1. However, we may still die on an
* ERR packet (if requested).
*/
#define PACKET_READ_GENTLE_ON_EOF (1u<<0)
#define PACKET_READ_CHOMP_NEWLINE (1u<<1)
#define PACKET_READ_DIE_ON_ERR_PACKET (1u<<2)
#define PACKET_READ_GENTLE_ON_EOF (1u<<0)
#define PACKET_READ_CHOMP_NEWLINE (1u<<1)
#define PACKET_READ_DIE_ON_ERR_PACKET (1u<<2)
#define PACKET_READ_GENTLE_ON_READ_ERROR (1u<<3)
int packet_read(int fd, char **src_buffer, size_t *src_len, char
*buffer, unsigned size, int options);
@@ -131,7 +136,7 @@ char *packet_read_line_buf(char **src_buf, size_t *src_len, int *size);
/*
* Reads a stream of variable sized packets until a flush packet is detected.
*/
ssize_t read_packetized_to_strbuf(int fd_in, struct strbuf *sb_out);
ssize_t read_packetized_to_strbuf(int fd_in, struct strbuf *sb_out, int options);
/*
* Receive multiplexed output stream over git native protocol.

View File

@@ -2,12 +2,13 @@
#include "config.h"
#include "repository.h"
#include "midx.h"
#include "fsmonitor-ipc.h"
#define UPDATE_DEFAULT_BOOL(s,v) do { if (s == -1) { s = v; } } while(0)
void prepare_repo_settings(struct repository *r)
{
int value;
int value, feature_many_files = 0;
char *strval;
if (r->settings.initialized)
@@ -58,7 +59,11 @@ void prepare_repo_settings(struct repository *r)
r->settings.core_multi_pack_index = value;
UPDATE_DEFAULT_BOOL(r->settings.core_multi_pack_index, 1);
if (!repo_config_get_bool(r, "core.usebuiltinfsmonitor", &value) && value)
r->settings.use_builtin_fsmonitor = 1;
if (!repo_config_get_bool(r, "feature.manyfiles", &value) && value) {
feature_many_files = 1;
UPDATE_DEFAULT_BOOL(r->settings.index_version, 4);
UPDATE_DEFAULT_BOOL(r->settings.core_untracked_cache, UNTRACKED_CACHE_WRITE);
}
@@ -67,8 +72,12 @@ void prepare_repo_settings(struct repository *r)
r->settings.fetch_write_commit_graph = value;
UPDATE_DEFAULT_BOOL(r->settings.fetch_write_commit_graph, 0);
if (!repo_config_get_bool(r, "feature.experimental", &value) && value)
if (!repo_config_get_bool(r, "feature.experimental", &value) && value) {
UPDATE_DEFAULT_BOOL(r->settings.fetch_negotiation_algorithm, FETCH_NEGOTIATION_SKIPPING);
if (feature_many_files && fsmonitor_ipc__is_supported())
UPDATE_DEFAULT_BOOL(r->settings.use_builtin_fsmonitor,
1);
}
/* Hack for test programs like test-dump-untracked-cache */
if (ignore_untracked_cache_config)

View File

@@ -41,6 +41,8 @@ struct repo_settings {
enum fetch_negotiation_setting fetch_negotiation_algorithm;
int core_multi_pack_index;
int use_builtin_fsmonitor;
};
struct repository {

239
simple-ipc.h Normal file
View File

@@ -0,0 +1,239 @@
#ifndef GIT_SIMPLE_IPC_H
#define GIT_SIMPLE_IPC_H
/*
* See Documentation/technical/api-simple-ipc.txt
*/
#if defined(GIT_WINDOWS_NATIVE) || !defined(NO_UNIX_SOCKETS)
#define SUPPORTS_SIMPLE_IPC
#endif
#ifdef SUPPORTS_SIMPLE_IPC
#include "pkt-line.h"
/*
* Simple IPC Client Side API.
*/
enum ipc_active_state {
/*
* The pipe/socket exists and the daemon is waiting for connections.
*/
IPC_STATE__LISTENING = 0,
/*
* The pipe/socket exists, but the daemon is not listening.
* Perhaps it is very busy.
* Perhaps the daemon died without deleting the path.
* Perhaps it is shutting down and draining existing clients.
* Perhaps it is dead, but other clients are lingering and
* still holding a reference to the pathname.
*/
IPC_STATE__NOT_LISTENING,
/*
* The requested pathname is bogus and no amount of retries
* will fix that.
*/
IPC_STATE__INVALID_PATH,
/*
* The requested pathname is not found. This usually means
* that there is no daemon present.
*/
IPC_STATE__PATH_NOT_FOUND,
IPC_STATE__OTHER_ERROR,
};
struct ipc_client_connect_options {
/*
* Spin under timeout if the server is running but can't
* accept our connection yet. This should always be set
* unless you just want to poke the server and see if it
* is alive.
*/
unsigned int wait_if_busy:1;
/*
* Spin under timeout if the pipe/socket is not yet present
* on the file system. This is useful if we just started
* the service and need to wait for it to become ready.
*/
unsigned int wait_if_not_found:1;
/*
* Disallow chdir() when creating a Unix domain socket.
*/
unsigned int uds_disallow_chdir:1;
};
#define IPC_CLIENT_CONNECT_OPTIONS_INIT { \
.wait_if_busy = 0, \
.wait_if_not_found = 0, \
.uds_disallow_chdir = 0, \
}
/*
* Determine if a server is listening on this named pipe or socket using
* platform-specific logic. This might just probe the filesystem or it
* might make a trivial connection to the server using this pathname.
*/
enum ipc_active_state ipc_get_active_state(const char *path);
struct ipc_client_connection {
int fd;
};
/*
* Try to connect to the daemon on the named pipe or socket.
*
* Returns IPC_STATE__LISTENING and a connection handle.
*
* Otherwise, returns info to help decide whether to retry or to
* spawn/respawn the server.
*/
enum ipc_active_state ipc_client_try_connect(
const char *path,
const struct ipc_client_connect_options *options,
struct ipc_client_connection **p_connection);
void ipc_client_close_connection(struct ipc_client_connection *connection);
/*
* Used by the client to synchronously send and receive a message with
* the server on the provided client connection.
*
* Returns 0 when successful.
*
* Calls error() and returns non-zero otherwise.
*/
int ipc_client_send_command_to_connection(
struct ipc_client_connection *connection,
const char *message, struct strbuf *answer);
/*
* Used by the client to synchronously connect and send and receive a
* message to the server listening at the given path.
*
* Returns 0 when successful.
*
* Calls error() and returns non-zero otherwise.
*/
int ipc_client_send_command(const char *path,
const struct ipc_client_connect_options *options,
const char *message, struct strbuf *answer);
/*
* Simple IPC Server Side API.
*/
struct ipc_server_reply_data;
typedef int (ipc_server_reply_cb)(struct ipc_server_reply_data *,
const char *response,
size_t response_len);
/*
* Prototype for an application-supplied callback to process incoming
* client IPC messages and compose a reply. The `application_cb` should
* use the provided `reply_cb` and `reply_data` to send an IPC response
* back to the client. The `reply_cb` callback can be called multiple
* times for chunking purposes. A reply message is optional and may be
* omitted if not necessary for the application.
*
* The return value from the application callback is ignored.
* The value `SIMPLE_IPC_QUIT` can be used to shutdown the server.
*/
typedef int (ipc_server_application_cb)(void *application_data,
const char *request,
ipc_server_reply_cb *reply_cb,
struct ipc_server_reply_data *reply_data);
#define SIMPLE_IPC_QUIT -2
/*
* Opaque instance data to represent an IPC server instance.
*/
struct ipc_server_data;
/*
* Control parameters for the IPC server instance.
* Use this to hide platform-specific settings.
*/
struct ipc_server_opts
{
int nr_threads;
/*
* Disallow chdir() when creating a Unix domain socket.
*/
unsigned int uds_disallow_chdir:1;
};
/*
* Start an IPC server instance in one or more background threads
* and return a handle to the pool.
*
* Returns 0 if the asynchronous server pool was started successfully.
* Returns -1 if not.
* Returns -2 if we could not startup because another server is using
* the socket or named pipe.
*
* When a client IPC message is received, the `application_cb` will be
* called (possibly on a random thread) to handle the message and
* optionally compose a reply message.
*/
int ipc_server_run_async(struct ipc_server_data **returned_server_data,
const char *path, const struct ipc_server_opts *opts,
ipc_server_application_cb *application_cb,
void *application_data);
/*
* Gently signal the IPC server pool to shutdown. No new client
* connections will be accepted, but existing connections will be
* allowed to complete.
*/
int ipc_server_stop_async(struct ipc_server_data *server_data);
/*
* Block the calling thread until all threads in the IPC server pool
* have completed and been joined.
*/
int ipc_server_await(struct ipc_server_data *server_data);
/*
* Close and free all resource handles associated with the IPC server
* pool.
*/
void ipc_server_free(struct ipc_server_data *server_data);
/*
* Run an IPC server instance and block the calling thread of the
* current process. It does not return until the IPC server has
* either shutdown or had an unrecoverable error.
*
* The IPC server handles incoming IPC messages from client processes
* and may use one or more background threads as necessary.
*
* Returns 0 after the server has completed successfully.
* Returns -1 if the server cannot be started.
* Returns -2 if we could not startup because another server is using
* the socket or named pipe.
*
* When a client IPC message is received, the `application_cb` will be
* called (possibly on a random thread) to handle the message and
* optionally compose a reply message.
*
* Note that `ipc_server_run()` is a synchronous wrapper around the
* above asynchronous routines. It effectively hides all of the
* server state and thread details from the caller and presents a
* simple synchronous interface.
*/
int ipc_server_run(const char *path, const struct ipc_server_opts *opts,
ipc_server_application_cb *application_cb,
void *application_data);
#endif /* SUPPORTS_SIMPLE_IPC */
#endif /* GIT_SIMPLE_IPC_H */

787
t/helper/test-simple-ipc.c Normal file
View File

@@ -0,0 +1,787 @@
/*
* test-simple-ipc.c: verify that the Inter-Process Communication works.
*/
#include "test-tool.h"
#include "cache.h"
#include "strbuf.h"
#include "simple-ipc.h"
#include "parse-options.h"
#include "thread-utils.h"
#include "strvec.h"
#ifndef SUPPORTS_SIMPLE_IPC
int cmd__simple_ipc(int argc, const char **argv)
{
die("simple IPC not available on this platform");
}
#else
/*
* The test daemon defines an "application callback" that supports a
* series of commands (see `test_app_cb()`).
*
* Unknown commands are caught here and we send an error message back
* to the client process.
*/
static int app__unhandled_command(const char *command,
ipc_server_reply_cb *reply_cb,
struct ipc_server_reply_data *reply_data)
{
struct strbuf buf = STRBUF_INIT;
int ret;
strbuf_addf(&buf, "unhandled command: %s", command);
ret = reply_cb(reply_data, buf.buf, buf.len);
strbuf_release(&buf);
return ret;
}
/*
* Reply with a single very large buffer. This is to ensure that
* long response are properly handled -- whether the chunking occurs
* in the kernel or in the (probably pkt-line) layer.
*/
#define BIG_ROWS (10000)
static int app__big_command(ipc_server_reply_cb *reply_cb,
struct ipc_server_reply_data *reply_data)
{
struct strbuf buf = STRBUF_INIT;
int row;
int ret;
for (row = 0; row < BIG_ROWS; row++)
strbuf_addf(&buf, "big: %.75d\n", row);
ret = reply_cb(reply_data, buf.buf, buf.len);
strbuf_release(&buf);
return ret;
}
/*
* Reply with a series of lines. This is to ensure that we can incrementally
* compute the response and chunk it to the client.
*/
#define CHUNK_ROWS (10000)
static int app__chunk_command(ipc_server_reply_cb *reply_cb,
struct ipc_server_reply_data *reply_data)
{
struct strbuf buf = STRBUF_INIT;
int row;
int ret;
for (row = 0; row < CHUNK_ROWS; row++) {
strbuf_setlen(&buf, 0);
strbuf_addf(&buf, "big: %.75d\n", row);
ret = reply_cb(reply_data, buf.buf, buf.len);
}
strbuf_release(&buf);
return ret;
}
/*
* Slowly reply with a series of lines. This is to model an expensive to
* compute chunked response (which might happen if this callback is running
* in a thread and is fighting for a lock with other threads).
*/
#define SLOW_ROWS (1000)
#define SLOW_DELAY_MS (10)
static int app__slow_command(ipc_server_reply_cb *reply_cb,
struct ipc_server_reply_data *reply_data)
{
struct strbuf buf = STRBUF_INIT;
int row;
int ret;
for (row = 0; row < SLOW_ROWS; row++) {
strbuf_setlen(&buf, 0);
strbuf_addf(&buf, "big: %.75d\n", row);
ret = reply_cb(reply_data, buf.buf, buf.len);
sleep_millisec(SLOW_DELAY_MS);
}
strbuf_release(&buf);
return ret;
}
/*
* The client sent a command followed by a (possibly very) large buffer.
*/
static int app__sendbytes_command(const char *received,
ipc_server_reply_cb *reply_cb,
struct ipc_server_reply_data *reply_data)
{
struct strbuf buf_resp = STRBUF_INIT;
const char *p = "?";
int len_ballast = 0;
int k;
int errs = 0;
int ret;
if (skip_prefix(received, "sendbytes ", &p))
len_ballast = strlen(p);
/*
* Verify that the ballast is n copies of a single letter.
* And that the multi-threaded IO layer didn't cross the streams.
*/
for (k = 1; k < len_ballast; k++)
if (p[k] != p[0])
errs++;
if (errs)
strbuf_addf(&buf_resp, "errs:%d\n", errs);
else
strbuf_addf(&buf_resp, "rcvd:%c%08d\n", p[0], len_ballast);
ret = reply_cb(reply_data, buf_resp.buf, buf_resp.len);
strbuf_release(&buf_resp);
return ret;
}
/*
* An arbitrary fixed address to verify that the application instance
* data is handled properly.
*/
static int my_app_data = 42;
static ipc_server_application_cb test_app_cb;
/*
* This is the "application callback" that sits on top of the
* "ipc-server". It completely defines the set of commands supported
* by this application.
*/
static int test_app_cb(void *application_data,
const char *command,
ipc_server_reply_cb *reply_cb,
struct ipc_server_reply_data *reply_data)
{
/*
* Verify that we received the application-data that we passed
* when we started the ipc-server. (We have several layers of
* callbacks calling callbacks and it's easy to get things mixed
* up (especially when some are "void*").)
*/
if (application_data != (void*)&my_app_data)
BUG("application_cb: application_data pointer wrong");
if (!strcmp(command, "quit")) {
/*
* The client sent a "quit" command. This is an async
* request for the server to shutdown.
*
* We DO NOT send the client a response message
* (because we have nothing to say and the other
* server threads have not yet stopped).
*
* Tell the ipc-server layer to start shutting down.
* This includes: stop listening for new connections
* on the socket/pipe and telling all worker threads
* to finish/drain their outgoing responses to other
* clients.
*
* This DOES NOT force an immediate sync shutdown.
*/
return SIMPLE_IPC_QUIT;
}
if (!strcmp(command, "ping")) {
const char *answer = "pong";
return reply_cb(reply_data, answer, strlen(answer));
}
if (!strcmp(command, "big"))
return app__big_command(reply_cb, reply_data);
if (!strcmp(command, "chunk"))
return app__chunk_command(reply_cb, reply_data);
if (!strcmp(command, "slow"))
return app__slow_command(reply_cb, reply_data);
if (starts_with(command, "sendbytes "))
return app__sendbytes_command(command, reply_cb, reply_data);
return app__unhandled_command(command, reply_cb, reply_data);
}
struct cl_args
{
const char *subcommand;
const char *path;
const char *token;
int nr_threads;
int max_wait_sec;
int bytecount;
int batchsize;
char bytevalue;
};
static struct cl_args cl_args = {
.subcommand = NULL,
.path = "ipc-test",
.token = NULL,
.nr_threads = 5,
.max_wait_sec = 60,
.bytecount = 1024,
.batchsize = 10,
.bytevalue = 'x',
};
/*
* This process will run as a simple-ipc server and listen for IPC commands
* from client processes.
*/
static int daemon__run_server(void)
{
int ret;
struct ipc_server_opts opts = {
.nr_threads = cl_args.nr_threads,
};
/*
* Synchronously run the ipc-server. We don't need any application
* instance data, so pass an arbitrary pointer (that we'll later
* verify made the round trip).
*/
ret = ipc_server_run(cl_args.path, &opts, test_app_cb, (void*)&my_app_data);
if (ret == -2)
error(_("socket/pipe already in use: '%s'"), cl_args.path);
else if (ret == -1)
error_errno(_("could not start server on: '%s'"), cl_args.path);
return ret;
}
#ifndef GIT_WINDOWS_NATIVE
/*
* This is adapted from `daemonize()`. Use `fork()` to directly create and
* run the daemon in a child process.
*/
static int spawn_server(pid_t *pid)
{
struct ipc_server_opts opts = {
.nr_threads = cl_args.nr_threads,
};
*pid = fork();
switch (*pid) {
case 0:
if (setsid() == -1)
error_errno(_("setsid failed"));
close(0);
close(1);
close(2);
sanitize_stdfds();
return ipc_server_run(cl_args.path, &opts, test_app_cb,
(void*)&my_app_data);
case -1:
return error_errno(_("could not spawn daemon in the background"));
default:
return 0;
}
}
#else
/*
* Conceptually like `daemonize()` but different because Windows does not
* have `fork(2)`. Spawn a normal Windows child process but without the
* limitations of `start_command()` and `finish_command()`.
*/
static int spawn_server(pid_t *pid)
{
char test_tool_exe[MAX_PATH];
struct strvec args = STRVEC_INIT;
int in, out;
GetModuleFileNameA(NULL, test_tool_exe, MAX_PATH);
in = open("/dev/null", O_RDONLY);
out = open("/dev/null", O_WRONLY);
strvec_push(&args, test_tool_exe);
strvec_push(&args, "simple-ipc");
strvec_push(&args, "run-daemon");
strvec_pushf(&args, "--name=%s", cl_args.path);
strvec_pushf(&args, "--threads=%d", cl_args.nr_threads);
*pid = mingw_spawnvpe(args.v[0], args.v, NULL, NULL, in, out, out);
close(in);
close(out);
strvec_clear(&args);
if (*pid < 0)
return error(_("could not spawn daemon in the background"));
return 0;
}
#endif
/*
* This is adapted from `wait_or_whine()`. Watch the child process and
* let it get started and begin listening for requests on the socket
* before reporting our success.
*/
static int wait_for_server_startup(pid_t pid_child)
{
int status;
pid_t pid_seen;
enum ipc_active_state s;
time_t time_limit, now;
time(&time_limit);
time_limit += cl_args.max_wait_sec;
for (;;) {
pid_seen = waitpid(pid_child, &status, WNOHANG);
if (pid_seen == -1)
return error_errno(_("waitpid failed"));
else if (pid_seen == 0) {
/*
* The child is still running (this should be
* the normal case). Try to connect to it on
* the socket and see if it is ready for
* business.
*
* If there is another daemon already running,
* our child will fail to start (possibly
* after a timeout on the lock), but we don't
* care (who responds) if the socket is live.
*/
s = ipc_get_active_state(cl_args.path);
if (s == IPC_STATE__LISTENING)
return 0;
time(&now);
if (now > time_limit)
return error(_("daemon not online yet"));
continue;
}
else if (pid_seen == pid_child) {
/*
* The new child daemon process shutdown while
* it was starting up, so it is not listening
* on the socket.
*
* Try to ping the socket in the odd chance
* that another daemon started (or was already
* running) while our child was starting.
*
* Again, we don't care who services the socket.
*/
s = ipc_get_active_state(cl_args.path);
if (s == IPC_STATE__LISTENING)
return 0;
/*
* We don't care about the WEXITSTATUS() nor
* any of the WIF*(status) values because
* `cmd__simple_ipc()` does the `!!result`
* trick on all function return values.
*
* So it is sufficient to just report the
* early shutdown as an error.
*/
return error(_("daemon failed to start"));
}
else
return error(_("waitpid is confused"));
}
}
/*
* This process will start a simple-ipc server in a background process and
* wait for it to become ready. This is like `daemonize()` but gives us
* more control and better error reporting (and makes it easier to write
* unit tests).
*/
static int daemon__start_server(void)
{
pid_t pid_child;
int ret;
/*
* Run the actual daemon in a background process.
*/
ret = spawn_server(&pid_child);
if (pid_child <= 0)
return ret;
/*
* Let the parent wait for the child process to get started
* and begin listening for requests on the socket.
*/
ret = wait_for_server_startup(pid_child);
return ret;
}
/*
* This process will run a quick probe to see if a simple-ipc server
* is active on this path.
*
* Returns 0 if the server is alive.
*/
static int client__probe_server(void)
{
enum ipc_active_state s;
s = ipc_get_active_state(cl_args.path);
switch (s) {
case IPC_STATE__LISTENING:
return 0;
case IPC_STATE__NOT_LISTENING:
return error("no server listening at '%s'", cl_args.path);
case IPC_STATE__PATH_NOT_FOUND:
return error("path not found '%s'", cl_args.path);
case IPC_STATE__INVALID_PATH:
return error("invalid pipe/socket name '%s'", cl_args.path);
case IPC_STATE__OTHER_ERROR:
default:
return error("other error for '%s'", cl_args.path);
}
}
/*
* Send an IPC command token to an already-running server daemon and
* print the response.
*
* This is a simple 1 word command/token that `test_app_cb()` (in the
* daemon process) will understand.
*/
static int client__send_ipc(void)
{
const char *command = "(no-command)";
struct strbuf buf = STRBUF_INIT;
struct ipc_client_connect_options options
= IPC_CLIENT_CONNECT_OPTIONS_INIT;
if (cl_args.token && *cl_args.token)
command = cl_args.token;
options.wait_if_busy = 1;
options.wait_if_not_found = 0;
if (!ipc_client_send_command(cl_args.path, &options, command, &buf)) {
if (buf.len) {
printf("%s\n", buf.buf);
fflush(stdout);
}
strbuf_release(&buf);
return 0;
}
return error("failed to send '%s' to '%s'", command, cl_args.path);
}
/*
* Send an IPC command to an already-running server and ask it to
* shutdown. "send quit" is an async request and queues a shutdown
* event in the server, so we spin and wait here for it to actually
* shutdown to make the unit tests a little easier to write.
*/
static int client__stop_server(void)
{
int ret;
time_t time_limit, now;
enum ipc_active_state s;
time(&time_limit);
time_limit += cl_args.max_wait_sec;
cl_args.token = "quit";
ret = client__send_ipc();
if (ret)
return ret;
for (;;) {
sleep_millisec(100);
s = ipc_get_active_state(cl_args.path);
if (s != IPC_STATE__LISTENING) {
/*
* The socket/pipe is gone and/or has stopped
* responding. Lets assume that the daemon
* process has exited too.
*/
return 0;
}
time(&now);
if (now > time_limit)
return error(_("daemon has not shutdown yet"));
}
}
/*
* Send an IPC command followed by ballast to confirm that a large
* message can be sent and that the kernel or pkt-line layers will
* properly chunk it and that the daemon receives the entire message.
*/
static int do_sendbytes(int bytecount, char byte, const char *path,
const struct ipc_client_connect_options *options)
{
struct strbuf buf_send = STRBUF_INIT;
struct strbuf buf_resp = STRBUF_INIT;
strbuf_addstr(&buf_send, "sendbytes ");
strbuf_addchars(&buf_send, byte, bytecount);
if (!ipc_client_send_command(path, options, buf_send.buf, &buf_resp)) {
strbuf_rtrim(&buf_resp);
printf("sent:%c%08d %s\n", byte, bytecount, buf_resp.buf);
fflush(stdout);
strbuf_release(&buf_send);
strbuf_release(&buf_resp);
return 0;
}
return error("client failed to sendbytes(%d, '%c') to '%s'",
bytecount, byte, path);
}
/*
* Send an IPC command with ballast to an already-running server daemon.
*/
static int client__sendbytes(void)
{
struct ipc_client_connect_options options
= IPC_CLIENT_CONNECT_OPTIONS_INIT;
options.wait_if_busy = 1;
options.wait_if_not_found = 0;
options.uds_disallow_chdir = 0;
return do_sendbytes(cl_args.bytecount, cl_args.bytevalue, cl_args.path,
&options);
}
struct multiple_thread_data {
pthread_t pthread_id;
struct multiple_thread_data *next;
const char *path;
int bytecount;
int batchsize;
int sum_errors;
int sum_good;
char letter;
};
static void *multiple_thread_proc(void *_multiple_thread_data)
{
struct multiple_thread_data *d = _multiple_thread_data;
int k;
struct ipc_client_connect_options options
= IPC_CLIENT_CONNECT_OPTIONS_INIT;
options.wait_if_busy = 1;
options.wait_if_not_found = 0;
/*
* A multi-threaded client should not be randomly calling chdir().
* The test will pass without this restriction because the test is
* not otherwise accessing the filesystem, but it makes us honest.
*/
options.uds_disallow_chdir = 1;
trace2_thread_start("multiple");
for (k = 0; k < d->batchsize; k++) {
if (do_sendbytes(d->bytecount + k, d->letter, d->path, &options))
d->sum_errors++;
else
d->sum_good++;
}
trace2_thread_exit();
return NULL;
}
/*
* Start a client-side thread pool. Each thread sends a series of
* IPC requests. Each request is on a new connection to the server.
*/
static int client__multiple(void)
{
struct multiple_thread_data *list = NULL;
int k;
int sum_join_errors = 0;
int sum_thread_errors = 0;
int sum_good = 0;
for (k = 0; k < cl_args.nr_threads; k++) {
struct multiple_thread_data *d = xcalloc(1, sizeof(*d));
d->next = list;
d->path = cl_args.path;
d->bytecount = cl_args.bytecount + cl_args.batchsize*(k/26);
d->batchsize = cl_args.batchsize;
d->sum_errors = 0;
d->sum_good = 0;
d->letter = 'A' + (k % 26);
if (pthread_create(&d->pthread_id, NULL, multiple_thread_proc, d)) {
warning("failed to create thread[%d] skipping remainder", k);
free(d);
break;
}
list = d;
}
while (list) {
struct multiple_thread_data *d = list;
if (pthread_join(d->pthread_id, NULL))
sum_join_errors++;
sum_thread_errors += d->sum_errors;
sum_good += d->sum_good;
list = d->next;
free(d);
}
printf("client (good %d) (join %d), (errors %d)\n",
sum_good, sum_join_errors, sum_thread_errors);
return (sum_join_errors + sum_thread_errors) ? 1 : 0;
}
int cmd__simple_ipc(int argc, const char **argv)
{
const char * const simple_ipc_usage[] = {
N_("test-helper simple-ipc is-active [<name>] [<options>]"),
N_("test-helper simple-ipc run-daemon [<name>] [<threads>]"),
N_("test-helper simple-ipc start-daemon [<name>] [<threads>] [<max-wait>]"),
N_("test-helper simple-ipc stop-daemon [<name>] [<max-wait>]"),
N_("test-helper simple-ipc send [<name>] [<token>]"),
N_("test-helper simple-ipc sendbytes [<name>] [<bytecount>] [<byte>]"),
N_("test-helper simple-ipc multiple [<name>] [<threads>] [<bytecount>] [<batchsize>]"),
NULL
};
const char *bytevalue = NULL;
struct option options[] = {
#ifndef GIT_WINDOWS_NATIVE
OPT_STRING(0, "name", &cl_args.path, N_("name"), N_("name or pathname of unix domain socket")),
#else
OPT_STRING(0, "name", &cl_args.path, N_("name"), N_("named-pipe name")),
#endif
OPT_INTEGER(0, "threads", &cl_args.nr_threads, N_("number of threads in server thread pool")),
OPT_INTEGER(0, "max-wait", &cl_args.max_wait_sec, N_("seconds to wait for daemon to start or stop")),
OPT_INTEGER(0, "bytecount", &cl_args.bytecount, N_("number of bytes")),
OPT_INTEGER(0, "batchsize", &cl_args.batchsize, N_("number of requests per thread")),
OPT_STRING(0, "byte", &bytevalue, N_("byte"), N_("ballast character")),
OPT_STRING(0, "token", &cl_args.token, N_("token"), N_("command token to send to the server")),
OPT_END()
};
if (argc < 2)
usage_with_options(simple_ipc_usage, options);
if (argc == 2 && !strcmp(argv[1], "-h"))
usage_with_options(simple_ipc_usage, options);
if (argc == 2 && !strcmp(argv[1], "SUPPORTS_SIMPLE_IPC"))
return 0;
cl_args.subcommand = argv[1];
argc--;
argv++;
argc = parse_options(argc, argv, NULL, options, simple_ipc_usage, 0);
if (cl_args.nr_threads < 1)
cl_args.nr_threads = 1;
if (cl_args.max_wait_sec < 0)
cl_args.max_wait_sec = 0;
if (cl_args.bytecount < 1)
cl_args.bytecount = 1;
if (cl_args.batchsize < 1)
cl_args.batchsize = 1;
if (bytevalue && *bytevalue)
cl_args.bytevalue = bytevalue[0];
/*
* Use '!!' on all dispatch functions to map from `error()` style
* (returns -1) style to `test_must_fail` style (expects 1). This
* makes shell error messages less confusing.
*/
if (!strcmp(cl_args.subcommand, "is-active"))
return !!client__probe_server();
if (!strcmp(cl_args.subcommand, "run-daemon"))
return !!daemon__run_server();
if (!strcmp(cl_args.subcommand, "start-daemon"))
return !!daemon__start_server();
/*
* Client commands follow. Ensure a server is running before
* sending any data. This might be overkill, but then again
* this is a test harness.
*/
if (!strcmp(cl_args.subcommand, "stop-daemon")) {
if (client__probe_server())
return 1;
return !!client__stop_server();
}
if (!strcmp(cl_args.subcommand, "send")) {
if (client__probe_server())
return 1;
return !!client__send_ipc();
}
if (!strcmp(cl_args.subcommand, "sendbytes")) {
if (client__probe_server())
return 1;
return !!client__sendbytes();
}
if (!strcmp(cl_args.subcommand, "multiple")) {
if (client__probe_server())
return 1;
return !!client__multiple();
}
die("Unhandled subcommand: '%s'", cl_args.subcommand);
}
#endif

View File

@@ -65,6 +65,7 @@ static struct test_cmd cmds[] = {
{ "sha1", cmd__sha1 },
{ "sha256", cmd__sha256 },
{ "sigchain", cmd__sigchain },
{ "simple-ipc", cmd__simple_ipc },
{ "strcmp-offset", cmd__strcmp_offset },
{ "string-list", cmd__string_list },
{ "submodule-config", cmd__submodule_config },

View File

@@ -55,6 +55,7 @@ int cmd__sha1(int argc, const char **argv);
int cmd__oid_array(int argc, const char **argv);
int cmd__sha256(int argc, const char **argv);
int cmd__sigchain(int argc, const char **argv);
int cmd__simple_ipc(int argc, const char **argv);
int cmd__strcmp_offset(int argc, const char **argv);
int cmd__string_list(int argc, const char **argv);
int cmd__submodule_config(int argc, const char **argv);

View File

@@ -24,7 +24,8 @@ test_description="Test core.fsmonitor"
# GIT_PERF_7519_SPLIT_INDEX: used to configure core.splitIndex
# GIT_PERF_7519_FSMONITOR: used to configure core.fsMonitor. May be an
# absolute path to an integration. May be a space delimited list of
# absolute paths to integrations.
# absolute paths to integrations. (This hook or list of hooks does not
# include the built-in fsmonitor--daemon.)
#
# The big win for using fsmonitor is the elimination of the need to scan the
# working directory looking for changed and untracked files. If the file
@@ -135,10 +136,16 @@ test_expect_success "one time repo setup" '
setup_for_fsmonitor() {
# set INTEGRATION_SCRIPT depending on the environment
if test -n "$INTEGRATION_PATH"
if test -n "$USE_FSMONITOR_DAEMON"
then
git config core.useBuiltinFSMonitor true &&
INTEGRATION_SCRIPT=false
elif test -n "$INTEGRATION_PATH"
then
git config core.useBuiltinFSMonitor false &&
INTEGRATION_SCRIPT="$INTEGRATION_PATH"
else
git config core.useBuiltinFSMonitor false &&
#
# Choose integration script based on existence of Watchman.
# Fall back to an empty integration script.
@@ -281,4 +288,30 @@ test_expect_success "setup without fsmonitor" '
test_fsmonitor_suite
trace_stop
#
# Run a full set of perf tests using the built-in fsmonitor--daemon.
# It does not use the Hook API, so it has a different setup.
# Explicitly start the daemon here and before we start client commands
# so that we can later add custom tracing.
#
test_lazy_prereq HAVE_FSMONITOR_DAEMON '
git version --build-options | grep "feature:" | grep "fsmonitor--daemon"
'
if test_have_prereq HAVE_FSMONITOR_DAEMON
then
USE_FSMONITOR_DAEMON=t
trace_start fsmonitor--daemon--server
git fsmonitor--daemon --start
trace_start fsmonitor--daemon--client
test_expect_success "setup for fsmonitor--daemon" 'setup_for_fsmonitor'
test_fsmonitor_suite
git fsmonitor--daemon --stop
trace_stop
fi
test_done

122
t/t0052-simple-ipc.sh Executable file
View File

@@ -0,0 +1,122 @@
#!/bin/sh
test_description='simple command server'
. ./test-lib.sh
test-tool simple-ipc SUPPORTS_SIMPLE_IPC || {
skip_all='simple IPC not supported on this platform'
test_done
}
stop_simple_IPC_server () {
test-tool simple-ipc stop-daemon
}
test_expect_success 'start simple command server' '
test_atexit stop_simple_IPC_server &&
test-tool simple-ipc start-daemon --threads=8 &&
test-tool simple-ipc is-active
'
test_expect_success 'simple command server' '
test-tool simple-ipc send --token=ping >actual &&
echo pong >expect &&
test_cmp expect actual
'
test_expect_success 'servers cannot share the same path' '
test_must_fail test-tool simple-ipc run-daemon &&
test-tool simple-ipc is-active
'
test_expect_success 'big response' '
test-tool simple-ipc send --token=big >actual &&
test_line_count -ge 10000 actual &&
grep -q "big: [0]*9999\$" actual
'
test_expect_success 'chunk response' '
test-tool simple-ipc send --token=chunk >actual &&
test_line_count -ge 10000 actual &&
grep -q "big: [0]*9999\$" actual
'
test_expect_success 'slow response' '
test-tool simple-ipc send --token=slow >actual &&
test_line_count -ge 100 actual &&
grep -q "big: [0]*99\$" actual
'
# Send an IPC with n=100,000 bytes of ballast. This should be large enough
# to force both the kernel and the pkt-line layer to chunk the message to the
# daemon and for the daemon to receive it in chunks.
#
test_expect_success 'sendbytes' '
test-tool simple-ipc sendbytes --bytecount=100000 --byte=A >actual &&
grep "sent:A00100000 rcvd:A00100000" actual
'
# Start a series of <threads> client threads that each make <batchsize>
# IPC requests to the server. Each (<threads> * <batchsize>) request
# will open a new connection to the server and randomly bind to a server
# thread. Each client thread exits after completing its batch. So the
# total number of live client threads will be smaller than the total.
# Each request will send a message containing at least <bytecount> bytes
# of ballast. (Responses are small.)
#
# The purpose here is to test threading in the server and responding to
# many concurrent client requests (regardless of whether they come from
# 1 client process or many). And to test that the server side of the
# named pipe/socket is stable. (On Windows this means that the server
# pipe is properly recycled.)
#
# On Windows it also lets us adjust the connection timeout in the
# `ipc_client_send_command()`.
#
# Note it is easy to drive the system into failure by requesting an
# insane number of threads on client or server and/or increasing the
# per-thread batchsize or the per-request bytecount (ballast).
# On Windows these failures look like "pipe is busy" errors.
# So I've chosen fairly conservative values for now.
#
# We expect output of the form "sent:<letter><length> ..."
# With terms (7, 19, 13) we expect:
# <letter> in [A-G]
# <length> in [19+0 .. 19+(13-1)]
# and (7 * 13) successful responses.
#
test_expect_success 'stress test threads' '
test-tool simple-ipc multiple \
--threads=7 \
--bytecount=19 \
--batchsize=13 \
>actual &&
test_line_count = 92 actual &&
grep "good 91" actual &&
grep "sent:A" <actual >actual_a &&
cat >expect_a <<-EOF &&
sent:A00000019 rcvd:A00000019
sent:A00000020 rcvd:A00000020
sent:A00000021 rcvd:A00000021
sent:A00000022 rcvd:A00000022
sent:A00000023 rcvd:A00000023
sent:A00000024 rcvd:A00000024
sent:A00000025 rcvd:A00000025
sent:A00000026 rcvd:A00000026
sent:A00000027 rcvd:A00000027
sent:A00000028 rcvd:A00000028
sent:A00000029 rcvd:A00000029
sent:A00000030 rcvd:A00000030
sent:A00000031 rcvd:A00000031
EOF
test_cmp expect_a actual_a
'
test_expect_success 'stop-daemon works' '
test-tool simple-ipc stop-daemon &&
test_must_fail test-tool simple-ipc is-active &&
test_must_fail test-tool simple-ipc send --token=ping
'
test_done

582
t/t7527-builtin-fsmonitor.sh Executable file
View File

@@ -0,0 +1,582 @@
#!/bin/sh
test_description='built-in file system watcher'
. ./test-lib.sh
# Ask the fsmonitor daemon to insert a little delay before responding to
# client commands like `git status` and `git fsmonitor--daemon --query` to
# allow recent filesystem events to be received by the daemon. This helps
# the CI/PR builds be more stable.
#
# An arbitrary millisecond value.
#
GIT_TEST_FSMONITOR_CLIENT_DELAY=1000
export GIT_TEST_FSMONITOR_CLIENT_DELAY
git version --build-options | grep "feature:" | grep "fsmonitor--daemon" || {
skip_all="The built-in FSMonitor is not supported on this platform"
test_done
}
kill_repo () {
r=$1
git -C $r fsmonitor--daemon --stop >/dev/null 2>/dev/null
rm -rf $1
return 0
}
start_daemon () {
case "$#" in
1) r="-C $1";;
*) r="";
esac
git $r fsmonitor--daemon --start || return $?
git $r fsmonitor--daemon --is-running || return $?
return 0
}
test_expect_success 'explicit daemon start and stop' '
test_when_finished "kill_repo test_explicit" &&
git init test_explicit &&
start_daemon test_explicit &&
git -C test_explicit fsmonitor--daemon --stop &&
test_must_fail git -C test_explicit fsmonitor--daemon --is-running
'
test_expect_success 'implicit daemon start' '
test_when_finished "kill_repo test_implicit" &&
git init test_implicit &&
test_must_fail git -C test_implicit fsmonitor--daemon --is-running &&
# query will implicitly start the daemon.
#
# for test-script simplicity, we send a V1 timestamp rather than
# a V2 token. either way, the daemon response to any query contains
# a new V2 token. (the daemon may complain that we sent a V1 request,
# but this test case is only concerned with whether the daemon was
# implicitly started.)
GIT_TRACE2_EVENT="$PWD/.git/trace" \
git -C test_implicit fsmonitor--daemon --query 0 >actual &&
nul_to_q <actual >actual.filtered &&
grep "builtin:" actual.filtered &&
# confirm that a daemon was started in the background.
#
# since the mechanism for starting the background daemon is platform
# dependent, just confirm that the foreground command received a
# response from the daemon.
grep :\"query/response-length\" .git/trace &&
git -C test_implicit fsmonitor--daemon --is-running &&
git -C test_implicit fsmonitor--daemon --stop &&
test_must_fail git -C test_implicit fsmonitor--daemon --is-running
'
test_expect_success 'implicit daemon stop (delete .git)' '
test_when_finished "kill_repo test_implicit_1" &&
git init test_implicit_1 &&
start_daemon test_implicit_1 &&
# deleting the .git directory will implicitly stop the daemon.
rm -rf test_implicit_1/.git &&
# Create an empty .git directory so that the following Git command
# will stay relative to the `-C` directory. Without this, the Git
# command will (override the requested -C argument) and crawl out
# to the containing Git source tree. This would make the test
# result dependent upon whether we were using fsmonitor on our
# development worktree.
sleep 1 &&
mkdir test_implicit_1/.git &&
test_must_fail git -C test_implicit_1 fsmonitor--daemon --is-running
'
test_expect_success 'implicit daemon stop (rename .git)' '
test_when_finished "kill_repo test_implicit_2" &&
git init test_implicit_2 &&
start_daemon test_implicit_2 &&
# renaming the .git directory will implicitly stop the daemon.
mv test_implicit_2/.git test_implicit_2/.xxx &&
# Create an empty .git directory so that the following Git command
# will stay relative to the `-C` directory. Without this, the Git
# command will (override the requested -C argument) and crawl out
# to the containing Git source tree. This would make the test
# result dependent upon whether we were using fsmonitor on our
# development worktree.
sleep 1 &&
mkdir test_implicit_2/.git &&
test_must_fail git -C test_implicit_2 fsmonitor--daemon --is-running
'
test_expect_success 'cannot start multiple daemons' '
test_when_finished "kill_repo test_multiple" &&
git init test_multiple &&
start_daemon test_multiple &&
test_must_fail git -C test_multiple fsmonitor--daemon --start 2>actual &&
grep "fsmonitor--daemon is already running" actual &&
git -C test_multiple fsmonitor--daemon --stop &&
test_must_fail git -C test_multiple fsmonitor--daemon --is-running
'
test_expect_success 'setup' '
>tracked &&
>modified &&
>delete &&
>rename &&
mkdir dir1 &&
>dir1/tracked &&
>dir1/modified &&
>dir1/delete &&
>dir1/rename &&
mkdir dir2 &&
>dir2/tracked &&
>dir2/modified &&
>dir2/delete &&
>dir2/rename &&
mkdir dirtorename &&
>dirtorename/a &&
>dirtorename/b &&
cat >.gitignore <<-\EOF &&
.gitignore
expect*
actual*
flush*
trace*
EOF
git -c core.useBuiltinFSMonitor= add . &&
test_tick &&
git -c core.useBuiltinFSMonitor= commit -m initial &&
git config core.useBuiltinFSMonitor true
'
test_expect_success 'update-index implicitly starts daemon' '
test_must_fail git fsmonitor--daemon --is-running &&
GIT_TRACE2_EVENT="$PWD/.git/trace_implicit_1" \
git update-index --fsmonitor &&
git fsmonitor--daemon --is-running &&
test_might_fail git fsmonitor--daemon --stop &&
grep \"event\":\"start\".*\"fsmonitor--daemon\" .git/trace_implicit_1
'
test_expect_success 'status implicitly starts daemon' '
test_must_fail git fsmonitor--daemon --is-running &&
GIT_TRACE2_EVENT="$PWD/.git/trace_implicit_2" \
git status >actual &&
git fsmonitor--daemon --is-running &&
test_might_fail git fsmonitor--daemon --stop &&
grep \"event\":\"start\".*\"fsmonitor--daemon\" .git/trace_implicit_2
'
edit_files() {
echo 1 >modified
echo 2 >dir1/modified
echo 3 >dir2/modified
>dir1/untracked
}
delete_files() {
rm -f delete
rm -f dir1/delete
rm -f dir2/delete
}
create_files() {
echo 1 >new
echo 2 >dir1/new
echo 3 >dir2/new
}
rename_files() {
mv rename renamed
mv dir1/rename dir1/renamed
mv dir2/rename dir2/renamed
}
file_to_directory() {
rm -f delete
mkdir delete
echo 1 >delete/new
}
directory_to_file() {
rm -rf dir1
echo 1 >dir1
}
verify_status() {
git status >actual &&
GIT_INDEX_FILE=.git/fresh-index git read-tree master &&
GIT_INDEX_FILE=.git/fresh-index git -c core.useBuiltinFSMonitor= status >expect &&
test_cmp expect actual &&
echo HELLO AFTER &&
cat .git/trace &&
echo HELLO AFTER
}
# The next few test cases confirm that our fsmonitor daemon sees each type
# of OS filesystem notification that we care about. At this layer we just
# ensure we are getting the OS notifications and do not try to confirm what
# is reported by `git status`.
#
# We run a simple query after modifying the filesystem just to introduce
# a bit of a delay so that the trace logging from the daemon has time to
# get flushed to disk.
#
# We `reset` and `clean` at the bottom of each test (and before stopping the
# daemon) because these commands might implicitly restart the daemon.
clean_up_repo_and_stop_daemon () {
git reset --hard HEAD
git clean -fd
git fsmonitor--daemon --stop
rm -f .git/trace
}
test_expect_success 'edit some files' '
test_when_finished "clean_up_repo_and_stop_daemon" &&
(
GIT_TRACE_FSMONITOR="$PWD/.git/trace" &&
export GIT_TRACE_FSMONITOR &&
start_daemon
) &&
edit_files &&
git fsmonitor--daemon --query 0 >/dev/null 2>&1 &&
grep "^event: dir1/modified$" .git/trace &&
grep "^event: dir2/modified$" .git/trace &&
grep "^event: modified$" .git/trace &&
grep "^event: dir1/untracked$" .git/trace
'
test_expect_success 'create some files' '
test_when_finished "clean_up_repo_and_stop_daemon" &&
(
GIT_TRACE_FSMONITOR="$PWD/.git/trace" &&
export GIT_TRACE_FSMONITOR &&
start_daemon
) &&
create_files &&
git fsmonitor--daemon --query 0 >/dev/null 2>&1 &&
grep "^event: dir1/new$" .git/trace &&
grep "^event: dir2/new$" .git/trace &&
grep "^event: new$" .git/trace
'
test_expect_success 'delete some files' '
test_when_finished "clean_up_repo_and_stop_daemon" &&
(
GIT_TRACE_FSMONITOR="$PWD/.git/trace" &&
export GIT_TRACE_FSMONITOR &&
start_daemon
) &&
delete_files &&
git fsmonitor--daemon --query 0 >/dev/null 2>&1 &&
grep "^event: dir1/delete$" .git/trace &&
grep "^event: dir2/delete$" .git/trace &&
grep "^event: delete$" .git/trace
'
test_expect_success 'rename some files' '
test_when_finished "clean_up_repo_and_stop_daemon" &&
(
GIT_TRACE_FSMONITOR="$PWD/.git/trace" &&
export GIT_TRACE_FSMONITOR &&
start_daemon
) &&
rename_files &&
git fsmonitor--daemon --query 0 >/dev/null 2>&1 &&
grep "^event: dir1/rename$" .git/trace &&
grep "^event: dir2/rename$" .git/trace &&
grep "^event: rename$" .git/trace &&
grep "^event: dir1/renamed$" .git/trace &&
grep "^event: dir2/renamed$" .git/trace &&
grep "^event: renamed$" .git/trace
'
test_expect_success 'rename directory' '
test_when_finished "clean_up_repo_and_stop_daemon" &&
(
GIT_TRACE_FSMONITOR="$PWD/.git/trace" &&
export GIT_TRACE_FSMONITOR &&
start_daemon
) &&
mv dirtorename dirrenamed &&
git fsmonitor--daemon --query 0 >/dev/null 2>&1 &&
grep "^event: dirtorename/*$" .git/trace &&
grep "^event: dirrenamed/*$" .git/trace
'
test_expect_success 'file changes to directory' '
test_when_finished "clean_up_repo_and_stop_daemon" &&
(
GIT_TRACE_FSMONITOR="$PWD/.git/trace" &&
export GIT_TRACE_FSMONITOR &&
start_daemon
) &&
file_to_directory &&
git fsmonitor--daemon --query 0 >/dev/null 2>&1 &&
grep "^event: delete$" .git/trace &&
grep "^event: delete/new$" .git/trace
'
test_expect_success 'directory changes to a file' '
test_when_finished "clean_up_repo_and_stop_daemon" &&
(
GIT_TRACE_FSMONITOR="$PWD/.git/trace" &&
export GIT_TRACE_FSMONITOR &&
start_daemon
) &&
directory_to_file &&
git fsmonitor--daemon --query 0 >/dev/null 2>&1 &&
grep "^event: dir1$" .git/trace
'
# 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
# current token) and start collecting events under a new token.
#
# the 'git fsmonitor--daemon --flush' command can be used to send a "flush"
# message to a running daemon and ask it to do a flush/resync.
test_expect_success 'flush cached data' '
test_when_finished "kill_repo test_flush" &&
git init test_flush &&
(
GIT_TEST_FSMONITOR_TOKEN=true &&
export GIT_TEST_FSMONITOR_TOKEN &&
GIT_TRACE_FSMONITOR="$PWD/.git/trace_daemon" &&
export GIT_TRACE_FSMONITOR &&
start_daemon test_flush
) &&
# The daemon should have an initial token with no events in _0 and
# then a few (probably platform-specific number of) events in _1.
# These should both have the same <token_id>.
git -C test_flush fsmonitor--daemon --query "builtin:test_00000001:0" >actual_0 &&
nul_to_q <actual_0 >actual_q0 &&
touch test_flush/file_1 &&
touch test_flush/file_2 &&
git -C test_flush fsmonitor--daemon --query "builtin:test_00000001:0" >actual_1 &&
nul_to_q <actual_1 >actual_q1 &&
grep "file_1" actual_q1 &&
# Force a flush. This will change the <token_id>, reset the <seq_nr>, and
# flush the file data. Then create some events and ensure that the file
# again appears in the cache. It should have the new <token_id>.
git -C test_flush fsmonitor--daemon --flush >flush_0 &&
nul_to_q <flush_0 >flush_q0 &&
grep "^builtin:test_00000002:0Q/Q$" flush_q0 &&
git -C test_flush fsmonitor--daemon --query "builtin:test_00000002:0" >actual_2 &&
nul_to_q <actual_2 >actual_q2 &&
grep "^builtin:test_00000002:0Q$" actual_q2 &&
touch test_flush/file_3 &&
git -C test_flush fsmonitor--daemon --query "builtin:test_00000002:0" >actual_3 &&
nul_to_q <actual_3 >actual_q3 &&
grep "file_3" actual_q3
'
# The next few test cases create repos where the .git directory is NOT
# inside the one of the working directory. That is, where .git is a file
# that points to a directory elsewhere. This happens for submodules and
# non-primary worktrees.
test_expect_success 'setup worktree base' '
git init wt-base &&
echo 1 >wt-base/file1 &&
git -C wt-base add file1 &&
git -C wt-base commit -m "c1"
'
test_expect_success 'worktree with .git file' '
git -C wt-base worktree add ../wt-secondary &&
(
GIT_TRACE2_PERF="$PWD/trace2_wt_secondary" &&
export GIT_TRACE2_PERF &&
GIT_TRACE_FSMONITOR="$PWD/trace_wt_secondary" &&
export GIT_TRACE_FSMONITOR &&
start_daemon wt-secondary
) &&
git -C wt-secondary fsmonitor--daemon --stop &&
test_must_fail git -C wt-secondary fsmonitor--daemon --is-running
'
# TODO Repeat one of the "edit" tests on wt-secondary and confirm that
# TODO we get the same events and behavior -- that is, that fsmonitor--daemon
# TODO correctly listens to events on both the working directory and to the
# TODO referenced GITDIR.
test_expect_success 'cleanup worktrees' '
kill_repo wt-secondary &&
kill_repo wt-base
'
# The next few tests perform arbitrary/contrived file operations and
# confirm that status is correct. That is, that the data (or lack of
# data) from fsmonitor doesn't cause incorrect results. And doesn't
# cause incorrect results when the untracked-cache is enabled.
test_lazy_prereq UNTRACKED_CACHE '
{ git update-index --test-untracked-cache; ret=$?; } &&
test $ret -ne 1
'
test_expect_success 'Matrix: setup for untracked-cache,fsmonitor matrix' '
test_might_fail git config --unset core.useBuiltinFSMonitor &&
git update-index --no-fsmonitor &&
test_might_fail git fsmonitor--daemon --stop
'
matrix_clean_up_repo () {
git reset --hard HEAD
git clean -fd
}
matrix_try () {
uc=$1
fsm=$2
fn=$3
test_expect_success "Matrix[uc:$uc][fsm:$fsm] $fn" '
matrix_clean_up_repo &&
$fn &&
if test $uc = false -a $fsm = false
then
git status --porcelain=v1 >.git/expect.$fn
else
git status --porcelain=v1 >.git/actual.$fn
test_cmp .git/expect.$fn .git/actual.$fn
fi
'
return $?
}
uc_values="false"
test_have_prereq UNTRACKED_CACHE && uc_values="false true"
for uc_val in $uc_values
do
if test $uc_val = false
then
test_expect_success "Matrix[uc:$uc_val] disable untracked cache" '
git config core.untrackedcache false &&
git update-index --no-untracked-cache
'
else
test_expect_success "Matrix[uc:$uc_val] enable untracked cache" '
git config core.untrackedcache true &&
git update-index --untracked-cache
'
fi
fsm_values="false true"
for fsm_val in $fsm_values
do
if test $fsm_val = false
then
test_expect_success "Matrix[uc:$uc_val][fsm:$fsm_val] disable fsmonitor" '
test_might_fail git config --unset core.useBuiltinFSMonitor &&
git update-index --no-fsmonitor &&
test_might_fail git fsmonitor--daemon --stop 2>/dev/null
'
else
test_expect_success "Matrix[uc:$uc_val][fsm:$fsm_val] enable fsmonitor" '
git config core.useBuiltinFSMonitor true &&
git fsmonitor--daemon --start &&
git update-index --fsmonitor
'
fi
matrix_try $uc_val $fsm_val edit_files
matrix_try $uc_val $fsm_val delete_files
matrix_try $uc_val $fsm_val create_files
matrix_try $uc_val $fsm_val rename_files
matrix_try $uc_val $fsm_val file_to_directory
matrix_try $uc_val $fsm_val directory_to_file
done
done
test_done

View File

@@ -1,13 +1,7 @@
#include "cache.h"
#include "unix-socket.h"
static int unix_stream_socket(void)
{
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (fd < 0)
die_errno("unable to create socket");
return fd;
}
#define DEFAULT_UNIX_STREAM_LISTEN_BACKLOG (5)
static int chdir_len(const char *orig, int len)
{
@@ -36,16 +30,23 @@ static void unix_sockaddr_cleanup(struct unix_sockaddr_context *ctx)
}
static int unix_sockaddr_init(struct sockaddr_un *sa, const char *path,
struct unix_sockaddr_context *ctx)
struct unix_sockaddr_context *ctx,
int disallow_chdir)
{
int size = strlen(path) + 1;
ctx->orig_dir = NULL;
if (size > sizeof(sa->sun_path)) {
const char *slash = find_last_dir_sep(path);
const char *slash;
const char *dir;
struct strbuf cwd = STRBUF_INIT;
if (disallow_chdir) {
errno = ENAMETOOLONG;
return -1;
}
slash = find_last_dir_sep(path);
if (!slash) {
errno = ENAMETOOLONG;
return -1;
@@ -71,15 +72,18 @@ static int unix_sockaddr_init(struct sockaddr_un *sa, const char *path,
return 0;
}
int unix_stream_connect(const char *path)
int unix_stream_connect(const char *path, int disallow_chdir)
{
int fd, saved_errno;
int fd = -1, saved_errno;
struct sockaddr_un sa;
struct unix_sockaddr_context ctx;
if (unix_sockaddr_init(&sa, path, &ctx) < 0)
if (unix_sockaddr_init(&sa, path, &ctx, disallow_chdir) < 0)
return -1;
fd = unix_stream_socket();
fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (fd < 0)
goto fail;
if (connect(fd, (struct sockaddr *)&sa, sizeof(sa)) < 0)
goto fail;
unix_sockaddr_cleanup(&ctx);
@@ -87,28 +91,36 @@ int unix_stream_connect(const char *path)
fail:
saved_errno = errno;
if (fd != -1)
close(fd);
unix_sockaddr_cleanup(&ctx);
close(fd);
errno = saved_errno;
return -1;
}
int unix_stream_listen(const char *path)
int unix_stream_listen(const char *path,
const struct unix_stream_listen_opts *opts)
{
int fd, saved_errno;
int fd = -1, saved_errno;
int backlog;
struct sockaddr_un sa;
struct unix_sockaddr_context ctx;
unlink(path);
if (unix_sockaddr_init(&sa, path, &ctx) < 0)
if (unix_sockaddr_init(&sa, path, &ctx, opts->disallow_chdir) < 0)
return -1;
fd = unix_stream_socket();
fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (fd < 0)
goto fail;
if (bind(fd, (struct sockaddr *)&sa, sizeof(sa)) < 0)
goto fail;
if (listen(fd, 5) < 0)
backlog = opts->listen_backlog_size;
if (backlog <= 0)
backlog = DEFAULT_UNIX_STREAM_LISTEN_BACKLOG;
if (listen(fd, backlog) < 0)
goto fail;
unix_sockaddr_cleanup(&ctx);
@@ -116,8 +128,9 @@ int unix_stream_listen(const char *path)
fail:
saved_errno = errno;
if (fd != -1)
close(fd);
unix_sockaddr_cleanup(&ctx);
close(fd);
errno = saved_errno;
return -1;
}

View File

@@ -1,7 +1,15 @@
#ifndef UNIX_SOCKET_H
#define UNIX_SOCKET_H
int unix_stream_connect(const char *path);
int unix_stream_listen(const char *path);
struct unix_stream_listen_opts {
int listen_backlog_size;
unsigned int disallow_chdir:1;
};
#define UNIX_STREAM_LISTEN_OPTS_INIT { 0 }
int unix_stream_connect(const char *path, int disallow_chdir);
int unix_stream_listen(const char *path,
const struct unix_stream_listen_opts *opts);
#endif /* UNIX_SOCKET_H */

128
unix-stream-server.c Normal file
View File

@@ -0,0 +1,128 @@
#include "cache.h"
#include "lockfile.h"
#include "unix-socket.h"
#include "unix-stream-server.h"
#define DEFAULT_LOCK_TIMEOUT (100)
/*
* Try to connect to a unix domain socket at `path` (if it exists) and
* see if there is a server listening.
*
* We don't know if the socket exists, whether a server died and
* failed to cleanup, or whether we have a live server listening, so
* we "poke" it.
*
* We immediately hangup without sending/receiving any data because we
* don't know anything about the protocol spoken and don't want to
* block while writing/reading data. It is sufficient to just know
* that someone is listening.
*/
static int is_another_server_alive(const char *path,
const struct unix_stream_listen_opts *opts)
{
int fd = unix_stream_connect(path, opts->disallow_chdir);
if (fd >= 0) {
close(fd);
return 1;
}
return 0;
}
int unix_stream_server__create(
const char *path,
const struct unix_stream_listen_opts *opts,
long timeout_ms,
struct unix_stream_server_socket **new_server_socket)
{
struct lock_file lock = LOCK_INIT;
int fd_socket;
struct unix_stream_server_socket *server_socket;
*new_server_socket = NULL;
if (timeout_ms < 0)
timeout_ms = DEFAULT_LOCK_TIMEOUT;
/*
* Create a lock at "<path>.lock" if we can.
*/
if (hold_lock_file_for_update_timeout(&lock, path, 0, timeout_ms) < 0)
return -1;
/*
* If another server is listening on "<path>" give up. We do not
* want to create a socket and steal future connections from them.
*/
if (is_another_server_alive(path, opts)) {
rollback_lock_file(&lock);
errno = EADDRINUSE;
return -2;
}
/*
* Create and bind to a Unix domain socket at "<path>".
*/
fd_socket = unix_stream_listen(path, opts);
if (fd_socket < 0) {
int saved_errno = errno;
rollback_lock_file(&lock);
errno = saved_errno;
return -1;
}
server_socket = xcalloc(1, sizeof(*server_socket));
server_socket->path_socket = strdup(path);
server_socket->fd_socket = fd_socket;
lstat(path, &server_socket->st_socket);
*new_server_socket = server_socket;
/*
* Always rollback (just delete) "<path>.lock" because we already created
* "<path>" as a socket and do not want to commit_lock to do the atomic
* rename trick.
*/
rollback_lock_file(&lock);
return 0;
}
void unix_stream_server__free(
struct unix_stream_server_socket *server_socket)
{
if (!server_socket)
return;
if (server_socket->fd_socket >= 0) {
if (!unix_stream_server__was_stolen(server_socket))
unlink(server_socket->path_socket);
close(server_socket->fd_socket);
}
free(server_socket->path_socket);
free(server_socket);
}
int unix_stream_server__was_stolen(
struct unix_stream_server_socket *server_socket)
{
struct stat st_now;
if (!server_socket)
return 0;
if (lstat(server_socket->path_socket, &st_now) == -1)
return 1;
if (st_now.st_ino != server_socket->st_socket.st_ino)
return 1;
if (st_now.st_dev != server_socket->st_socket.st_dev)
return 1;
if (!S_ISSOCK(st_now.st_mode))
return 1;
return 0;
}

36
unix-stream-server.h Normal file
View File

@@ -0,0 +1,36 @@
#ifndef UNIX_STREAM_SERVER_H
#define UNIX_STREAM_SERVER_H
#include "unix-socket.h"
struct unix_stream_server_socket {
char *path_socket;
struct stat st_socket;
int fd_socket;
};
/*
* Create a Unix Domain Socket at the given path under the protection
* of a '.lock' lockfile.
*
* Returns 0 on success, -1 on error, -2 if socket is in use.
*/
int unix_stream_server__create(
const char *path,
const struct unix_stream_listen_opts *opts,
long timeout_ms,
struct unix_stream_server_socket **server_socket);
/*
* Close and delete the socket.
*/
void unix_stream_server__free(
struct unix_stream_server_socket *server_socket);
/*
* Return 1 if the inode of the pathname to our socket changes.
*/
int unix_stream_server__was_stolen(
struct unix_stream_server_socket *server_socket);
#endif /* UNIX_STREAM_SERVER_H */