mirror of
https://github.com/git-for-windows/git.git
synced 2026-03-16 11:02:37 -05:00
Merge pull request #3082 from dscho/fsmonitor-gfw
Add an experimental built-in FSMonitor
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -71,6 +71,7 @@
|
||||
/git-format-patch
|
||||
/git-fsck
|
||||
/git-fsck-objects
|
||||
/git-fsmonitor--daemon
|
||||
/git-gc
|
||||
/git-get-tar-commit-id
|
||||
/git-grep
|
||||
|
||||
@@ -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
|
||||
|
||||
107
Documentation/git-fsmonitor--daemon.txt
Normal file
107
Documentation/git-fsmonitor--daemon.txt
Normal 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
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
105
Documentation/technical/api-simple-ipc.txt
Normal file
105
Documentation/technical/api-simple-ipc.txt
Normal 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.
|
||||
24
Makefile
24
Makefile
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
1611
builtin/fsmonitor--daemon.c
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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"));
|
||||
|
||||
484
compat/fsmonitor/fsmonitor-fs-listen-macos.c
Normal file
484
compat/fsmonitor/fsmonitor-fs-listen-macos.c
Normal 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;
|
||||
}
|
||||
514
compat/fsmonitor/fsmonitor-fs-listen-win32.c
Normal file
514
compat/fsmonitor/fsmonitor-fs-listen-win32.c
Normal 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);
|
||||
}
|
||||
49
compat/fsmonitor/fsmonitor-fs-listen.h
Normal file
49
compat/fsmonitor/fsmonitor-fs-listen.h
Normal 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 */
|
||||
28
compat/simple-ipc/ipc-shared.c
Normal file
28
compat/simple-ipc/ipc-shared.c
Normal 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 */
|
||||
986
compat/simple-ipc/ipc-unix-socket.c
Normal file
986
compat/simple-ipc/ipc-unix-socket.c
Normal 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);
|
||||
}
|
||||
751
compat/simple-ipc/ipc-win32.c
Normal file
751
compat/simple-ipc/ipc-win32.c
Normal 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);
|
||||
}
|
||||
9
config.c
9
config.c
@@ -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)
|
||||
|
||||
2
config.h
2
config.h
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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})
|
||||
|
||||
11
convert.c
11
convert.c
@@ -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
142
fsmonitor--daemon.h
Normal 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
153
fsmonitor-ipc.c
Normal 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
48
fsmonitor-ipc.h
Normal 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 */
|
||||
32
fsmonitor.c
32
fsmonitor.c
@@ -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
1
git.c
@@ -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
4
help.c
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
58
pkt-line.c
58
pkt-line.c
@@ -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;
|
||||
|
||||
17
pkt-line.h
17
pkt-line.h
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
239
simple-ipc.h
Normal 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
787
t/helper/test-simple-ipc.c
Normal 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
|
||||
@@ -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 },
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
122
t/t0052-simple-ipc.sh
Executable 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
582
t/t7527-builtin-fsmonitor.sh
Executable 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
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
128
unix-stream-server.c
Normal 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
36
unix-stream-server.h
Normal 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 */
|
||||
Reference in New Issue
Block a user