Skip to contentSkip to conversation

fsmonitor: Implement fsmonitor for Linux

PATCH Superseded Eric DeCosta via GitGitGadget 9 participants 89 messages Oct 9, 2022 – Apr 12, 2023

Subsystems (2)
lore
Share & tools
lore
#0 lore raw

cover fsmonitor: Implement fsmonitor for Linux

Goal is to deliver fsmonitor for Linux that is on par with fsmonitor for Windows and Mac OS.

This patch set builds upon previous work for done for Windows and Mac OS (first 6 patches) to implement a fsmonitor back-end for Linux based on the Linux inotify API. inotify differs significantly from the equivalent Windows and Mac OS APIs in that a watch must be registered for every directory of interest (rather than a singular watch at the root of the directory tree) and special care must be taken to handle directory renames correctly.

More information about inotify:
https://man7.org/linux/man-pages/man7/inotify.7.html

Eric DeCosta (12):
fsmonitor: refactor filesystem checks to common interface
fsmonitor: relocate socket file if .git directory is remote
fsmonitor: avoid socket location check if using hook
fsmonitor: deal with synthetic firmlinks on macOS
fsmonitor: check for compatability before communicating with fsmonitor
fsmonitor: add documentation for allowRemote and socketDir options
fsmonitor: prepare to share code between Mac OS and Linux
fsmonitor: determine if filesystem is local or remote
fsmonitor: implement filesystem change listener for Linux
fsmonitor: enable fsmonitor for Linux
fsmonitor: test updates
fsmonitor: update doc for Linux

 Documentation/config.txt                      |   2 +
 Documentation/config/fsmonitor--daemon.txt    |  11 +
 Documentation/git-fsmonitor--daemon.txt       |  45 +-
 Makefile                                      |   6 +-
 builtin/fsmonitor--daemon.c                   |  11 +-
 ...{fsm-health-darwin.c => fsm-health-unix.c} |   0
 compat/fsmonitor/fsm-ipc-unix.c               |  52 ++
 compat/fsmonitor/fsm-ipc-win32.c              |   9 +
 compat/fsmonitor/fsm-listen-darwin.c          |  14 +-
 compat/fsmonitor/fsm-listen-linux.c           | 664 ++++++++++++++++++
 compat/fsmonitor/fsm-path-utils-darwin.c      | 135 ++++
 compat/fsmonitor/fsm-path-utils-linux.c       | 163 +++++
 compat/fsmonitor/fsm-path-utils-win32.c       | 145 ++++
 compat/fsmonitor/fsm-settings-darwin.c        |  89 ---
 compat/fsmonitor/fsm-settings-unix.c          |  62 ++
 compat/fsmonitor/fsm-settings-win32.c         | 174 +----
 config.mak.uname                              |  13 +
 contrib/buildsystems/CMakeLists.txt           |  19 +-
 fsmonitor--daemon.h                           |   3 +
 fsmonitor-ipc.c                               |  18 +-
 fsmonitor-ipc.h                               |   4 +-
 fsmonitor-path-utils.h                        |  60 ++
 fsmonitor-settings.c                          |  68 +-
 fsmonitor-settings.h                          |   4 +-
 fsmonitor.c                                   |   7 +
 t/t7527-builtin-fsmonitor.sh                  |  58 +-
 26 files changed, 1531 insertions(+), 305 deletions(-)
 create mode 100644 Documentation/config/fsmonitor--daemon.txt
 rename compat/fsmonitor/{fsm-health-darwin.c => fsm-health-unix.c} (100%)
 create mode 100644 compat/fsmonitor/fsm-ipc-unix.c
 create mode 100644 compat/fsmonitor/fsm-ipc-win32.c
 create mode 100644 compat/fsmonitor/fsm-listen-linux.c
 create mode 100644 compat/fsmonitor/fsm-path-utils-darwin.c
 create mode 100644 compat/fsmonitor/fsm-path-utils-linux.c
 create mode 100644 compat/fsmonitor/fsm-path-utils-win32.c
 delete mode 100644 compat/fsmonitor/fsm-settings-darwin.c
 create mode 100644 compat/fsmonitor/fsm-settings-unix.c
 create mode 100644 fsmonitor-path-utils.h

base-commit: 3dcec76d9df911ed8321007b1d197c1a206dc164
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-1352%2Fedecosta-mw%2Ffsmonitor_linux-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-1352/edecosta-mw/fsmonitor_linux-v1
Pull-Request: https://github.com/git/git/pull/1352

Signature · 1 line
gitgitgadget
#3 lore raw

PATCH 2/12 fsmonitor: relocate socket file if .git directory is remote

From: Eric DeCosta <[email protected]>

If the .git directory is on a remote filesystem, create the socket file in 'fsmonitor.socketDir' if it is defined, else create it in $HOME.

---
 Makefile                               |  1 +
 builtin/fsmonitor--daemon.c            |  3 +-
 compat/fsmonitor/fsm-ipc-darwin.c      | 52 ++++++++++++++++++++++++++
 compat/fsmonitor/fsm-ipc-win32.c       |  9 +++++
 compat/fsmonitor/fsm-settings-darwin.c |  2 +-
 contrib/buildsystems/CMakeLists.txt    |  2 +
 fsmonitor-ipc.c                        | 18 ++++-----
 fsmonitor-ipc.h                        |  4 +-
 8 files changed, 78 insertions(+), 13 deletions(-)
 create mode 100644 compat/fsmonitor/fsm-ipc-darwin.c
 create mode 100644 compat/fsmonitor/fsm-ipc-win32.c
Makefile +1 -0
index ffab427ea5b..feb675a6959 100644
@@ -2039,6 +2039,7 @@ ifdef FSMONITOR_DAEMON_BACKEND
 	COMPAT_CFLAGS += -DHAVE_FSMONITOR_DAEMON_BACKEND
 	COMPAT_OBJS += compat/fsmonitor/fsm-listen-$(FSMONITOR_DAEMON_BACKEND).o
 	COMPAT_OBJS += compat/fsmonitor/fsm-health-$(FSMONITOR_DAEMON_BACKEND).o
+	COMPAT_OBJS += compat/fsmonitor/fsm-ipc-$(FSMONITOR_DAEMON_BACKEND).o
 endif
 
 ifdef FSMONITOR_OS_SETTINGS
builtin/fsmonitor--daemon.c +2 -1
index 2c109cf8b37..0123fc33ed2 100644
@@ -1343,7 +1343,8 @@ static int fsmonitor_run_daemon(void)
 	 * directory.)
 	 */
 	strbuf_init(&state.path_ipc, 0);
-	strbuf_addstr(&state.path_ipc, absolute_path(fsmonitor_ipc__get_path()));
+	strbuf_addstr(&state.path_ipc,
+		absolute_path(fsmonitor_ipc__get_path(the_repository)));
 
 	/*
 	 * Confirm that we can create platform-specific resources for the
compat/fsmonitor/fsm-ipc-darwin.c new file +52 -0

52 lines not on this page · raw message

compat/fsmonitor/fsm-ipc-win32.c new file +9 -0
new file mode 100644
index 00000000000..e08c505c148
@@ -0,0 +1,9 @@
+#include "config.h"
+#include "fsmonitor-ipc.h"
+
+const char *fsmonitor_ipc__get_path(struct repository *r) {
+	static char *ret;
+	if (!ret)
+		ret = git_pathdup("fsmonitor--daemon.ipc");
+	return ret;
+}
compat/fsmonitor/fsm-settings-darwin.c +1 -1
index 699f0b272e6..7241c4c22c9 100644
@@ -26,7 +26,7 @@
 static enum fsmonitor_reason check_uds_volume(struct repository *r)
 {
 	struct fs_info fs;
-	const char *ipc_path = fsmonitor_ipc__get_path();
+	const char *ipc_path = fsmonitor_ipc__get_path(r);
 	struct strbuf path = STRBUF_INIT;
 	strbuf_add(&path, ipc_path, strlen(ipc_path));
 
contrib/buildsystems/CMakeLists.txt +2 -0
index 5482a04b3ce..787738e6fa3 100644
@@ -308,6 +308,7 @@ if(SUPPORTS_SIMPLE_IPC)
 		add_compile_definitions(HAVE_FSMONITOR_DAEMON_BACKEND)
 		list(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-win32.c)
 		list(APPEND compat_SOURCES compat/fsmonitor/fsm-health-win32.c)
+		list(APPEND compat_SOURCES compat/fsmonitor/fsm-ipc-win32.c)
 		list(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-win32.c)
 
 		add_compile_definitions(HAVE_FSMONITOR_OS_SETTINGS)
@@ -316,6 +317,7 @@ if(SUPPORTS_SIMPLE_IPC)
 		add_compile_definitions(HAVE_FSMONITOR_DAEMON_BACKEND)
 		list(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-darwin.c)
 		list(APPEND compat_SOURCES compat/fsmonitor/fsm-health-darwin.c)
+		list(APPEND compat_SOURCES compat/fsmonitor/fsm-ipc-darwin.c)
 		list(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-darwin.c)
 
 		add_compile_definitions(HAVE_FSMONITOR_OS_SETTINGS)
fsmonitor-ipc.c +8 -10
index 789e7397baa..c0f42301c84 100644
@@ -18,7 +18,7 @@ int fsmonitor_ipc__is_supported(void)
 	return 0;
 }
 
-const char *fsmonitor_ipc__get_path(void)
+const char *fsmonitor_ipc__get_path(struct repository *r)
 {
 	return NULL;
 }
@@ -47,11 +47,9 @@ int fsmonitor_ipc__is_supported(void)
 	return 1;
 }
 
-GIT_PATH_FUNC(fsmonitor_ipc__get_path, "fsmonitor--daemon.ipc")
-
 enum ipc_active_state fsmonitor_ipc__get_state(void)
 {
-	return ipc_get_active_state(fsmonitor_ipc__get_path());
+	return ipc_get_active_state(fsmonitor_ipc__get_path(the_repository));
 }
 
 static int spawn_daemon(void)
@@ -81,8 +79,8 @@ int fsmonitor_ipc__send_query(const char *since_token,
 	trace2_data_string("fsm_client", NULL, "query/command", tok);
 
 try_again:
-	state = ipc_client_try_connect(fsmonitor_ipc__get_path(), &options,
-				       &connection);
+	state = ipc_client_try_connect(fsmonitor_ipc__get_path(the_repository),
+						&options, &connection);
 
 	switch (state) {
 	case IPC_STATE__LISTENING:
@@ -117,13 +115,13 @@ try_again:
 
 	case IPC_STATE__INVALID_PATH:
 		ret = error(_("fsmonitor_ipc__send_query: invalid path '%s'"),
-			    fsmonitor_ipc__get_path());
+			    fsmonitor_ipc__get_path(the_repository));
 		goto done;
 
 	case IPC_STATE__OTHER_ERROR:
 	default:
 		ret = error(_("fsmonitor_ipc__send_query: unspecified error on '%s'"),
-			    fsmonitor_ipc__get_path());
+			    fsmonitor_ipc__get_path(the_repository));
 		goto done;
 	}
 
@@ -149,8 +147,8 @@ int fsmonitor_ipc__send_command(const char *command,
 	options.wait_if_busy = 1;
 	options.wait_if_not_found = 0;
 
-	state = ipc_client_try_connect(fsmonitor_ipc__get_path(), &options,
-				       &connection);
+	state = ipc_client_try_connect(fsmonitor_ipc__get_path(the_repository),
+						&options, &connection);
 	if (state != IPC_STATE__LISTENING) {
 		die(_("fsmonitor--daemon is not running"));
 		return -1;
fsmonitor-ipc.h +3 -1
index b6a7067c3af..8b489da762b 100644
@@ -3,6 +3,8 @@
 
 #include "simple-ipc.h"
 
+struct repository;
+
 /*
  * Returns true if built-in file system monitor daemon is defined
  * for this platform.
@@ -16,7 +18,7 @@ int fsmonitor_ipc__is_supported(void);
  *
  * Returns NULL if the daemon is not supported on this platform.
  */
-const char *fsmonitor_ipc__get_path(void);
+const char *fsmonitor_ipc__get_path(struct repository *r);
 
 /*
  * Try to determine whether there is a `git-fsmonitor--daemon` process
Signature · 1 line
gitgitgadget
#5 lore raw

PATCH 4/12 fsmonitor: deal with synthetic firmlinks on macOS

In reply to Eric DeCosta via GitGitGadget

From: Eric DeCosta <[email protected]>

Starting with macOS 10.15 (Catalina), Apple introduced a new feature called 'firmlinks' in order to separate the boot volume into two volumes, one read-only and one writable but still present them to the user as a single volume. Along with this change, Apple removed the ability to create symlinks in the root directory and replaced them with 'synthetic firmlinks'. See 'man synthetic.conf'

When FSEevents reports the path of changed files, if the path involves a synthetic firmlink, the path is reported from the point of the synthetic firmlink and not the real path. For example:

Real path:
/System/Volumes/Data/network/working/directory/foo.txt

Synthetic firmlink:
/network -> /System/Volumes/Data/network

FSEvents path:
/network/working/directory/foo.txt

This causes the FSEvents path to not match against the worktree directory.

There are several ways in which synthetic firmlinks can be created:
they can be defined in /etc/synthetic.conf, the automounter can create them, and there may be other means. Simply reading /etc/synthetic.conf is insufficient. No matter what process creates synthetic firmlinks, they all get created in the root directory.

Therefore, in order to deal with synthetic firmlinks, the root directory is scanned and the first possible synthetic firmink that, when resolved, is a prefix of the worktree is used to map FSEvents paths to worktree paths.

---
 builtin/fsmonitor--daemon.c              |  8 +++
 compat/fsmonitor/fsm-listen-darwin.c     | 14 +++-
 compat/fsmonitor/fsm-path-utils-darwin.c | 92 ++++++++++++++++++++++++
 compat/fsmonitor/fsm-path-utils-win32.c  | 17 +++++
 fsmonitor--daemon.h                      |  3 +
 fsmonitor-path-utils.h                   | 36 +++++++++-
 6 files changed, 167 insertions(+), 3 deletions(-)
builtin/fsmonitor--daemon.c +8 -0
index 0123fc33ed2..7a4cb78c7dd 100644
@@ -3,6 +3,7 @@
 #include "parse-options.h"
 #include "fsmonitor.h"
 #include "fsmonitor-ipc.h"
+#include "fsmonitor-path-utils.h"
 #include "compat/fsmonitor/fsm-health.h"
 #include "compat/fsmonitor/fsm-listen.h"
 #include "fsmonitor--daemon.h"
@@ -1282,6 +1283,11 @@ static int fsmonitor_run_daemon(void)
 	strbuf_addstr(&state.path_worktree_watch, absolute_path(get_git_work_tree()));
 	state.nr_paths_watching = 1;
 
+	strbuf_init(&state.alias.alias, 0);
+	strbuf_init(&state.alias.points_to, 0);
+	if ((err = fsmonitor__get_alias(state.path_worktree_watch.buf, &state.alias)))
+		goto done;
+
 	/*
 	 * We create and delete cookie files somewhere inside the .git
 	 * directory to help us keep sync with the file system.  If
@@ -1391,6 +1397,8 @@ done:
 	strbuf_release(&state.path_gitdir_watch);
 	strbuf_release(&state.path_cookie_prefix);
 	strbuf_release(&state.path_ipc);
+	strbuf_release(&state.alias.alias);
+	strbuf_release(&state.alias.points_to);
 
 	return err;
 }
compat/fsmonitor/fsm-listen-darwin.c +12 -2
index 8e208e8289e..daeee4e465c 100644
@@ -26,6 +26,7 @@
 #include "fsmonitor.h"
 #include "fsm-listen.h"
 #include "fsmonitor--daemon.h"
+#include "fsmonitor-path-utils.h"
 
 struct fsm_listen_data
 {
@@ -198,8 +199,9 @@ static void fsevent_callback(ConstFSEventStreamRef streamRef,
 	struct string_list cookie_list = STRING_LIST_INIT_DUP;
 	const char *path_k;
 	const char *slash;
-	int k;
+	char *resolved = NULL;
 	struct strbuf tmp = STRBUF_INIT;
+	int k;
 
 	/*
 	 * Build a list of all filesystem changes into a private/local
@@ -209,7 +211,12 @@ static void fsevent_callback(ConstFSEventStreamRef streamRef,
 		/*
 		 * On Mac, we receive an array of absolute paths.
 		 */
-		path_k = paths[k];
+		free(resolved);
+		resolved = fsmonitor__resolve_alias(paths[k], &state->alias);
+		if (resolved)
+			path_k = resolved;
+		else
+			path_k = paths[k];
 
 		/*
 		 * If you want to debug FSEvents, log them to GIT_TRACE_FSMONITOR.
@@ -238,6 +245,7 @@ static void fsevent_callback(ConstFSEventStreamRef streamRef,
 			fsmonitor_force_resync(state);
 			fsmonitor_batch__free_list(batch);
 			string_list_clear(&cookie_list, 0);
+			batch = NULL;
 
 			/*
 			 * We assume that any events that we received
@@ -360,12 +368,14 @@ static void fsevent_callback(ConstFSEventStreamRef streamRef,
 		}
 	}
 
+	free(resolved);
 	fsmonitor_publish(state, batch, &cookie_list);
 	string_list_clear(&cookie_list, 0);
 	strbuf_release(&tmp);
 	return;
 
 force_shutdown:
+	free(resolved);
 	fsmonitor_batch__free_list(batch);
 	string_list_clear(&cookie_list, 0);
 
compat/fsmonitor/fsm-path-utils-darwin.c +92 -0

100 lines not on this page · raw message

compat/fsmonitor/fsm-path-utils-win32.c +17 -0
index a90b8f7925b..0d95bbb416f 100644
@@ -126,3 +126,20 @@ int fsmonitor__is_fs_remote(const char *path)
 		return -1;
 	return fs.is_remote;
 }
+
+/*
+ * No-op for now.
+ */
+int fsmonitor__get_alias(const char *path, struct alias_info *info)
+{
+	return 0;
+}
+
+/*
+ * No-op for now.
+ */
+char *fsmonitor__resolve_alias(const char *path,
+	const struct alias_info *info)
+{
+	return NULL;
+}
fsmonitor--daemon.h +3 -0
index 2102a5c9ff5..e24838f9a86 100644
@@ -8,6 +8,7 @@
 #include "run-command.h"
 #include "simple-ipc.h"
 #include "thread-utils.h"
+#include "fsmonitor-path-utils.h"
 
 struct fsmonitor_batch;
 struct fsmonitor_token_data;
@@ -43,6 +44,7 @@ struct fsmonitor_daemon_state {
 
 	struct strbuf path_worktree_watch;
 	struct strbuf path_gitdir_watch;
+	struct alias_info alias;
 	int nr_paths_watching;
 
 	struct fsmonitor_token_data *current_token_data;
@@ -59,6 +61,7 @@ struct fsmonitor_daemon_state {
 
 	struct ipc_server_data *ipc_server_data;
 	struct strbuf path_ipc;
+
 };
 
 /*
fsmonitor-path-utils.h +35 -1
index 41edf5b934f..5bfdfb81c14 100644
@@ -1,13 +1,21 @@
 #ifndef FSM_PATH_UTILS_H
 #define FSM_PATH_UTILS_H
 
+#include "strbuf.h"
+
+struct alias_info
+{
+	struct strbuf alias;
+	struct strbuf points_to;
+};
+
 struct fs_info {
 	int is_remote;
 	char *typename;
 };
 
 /*
- * Get some basic filesystem informtion for the given path
+ * Get some basic filesystem information for the given path
  *
  * The caller owns the storage that is occupied by fs_info and
  * is responsible for releasing it.
@@ -23,4 +31,30 @@ int fsmonitor__get_fs_info(const char *path, struct fs_info *fs_info);
  */
 int fsmonitor__is_fs_remote(const char *path);
 
+/*
+ * Get the alias in given path, if any.
+ *
+ * Sets alias to the first alias that matches any part of the path.
+ *
+ * If an alias is found, info.alias and info.points_to are set to the
+ * found mapping.
+ *
+ * Returns -1 on error, 0 otherwise.
+ *
+ * The caller owns the storage that is occupied by info.alias and
+ * info.points_to and is responsible for releasing it.
+ */
+int fsmonitor__get_alias(const char *path, struct alias_info *info);
+
+/*
+ * Resolve the path against the given alias.
+ *
+ * Returns the resolved path if there is one, NULL otherwise.
+ *
+ * The caller owns the storage that the returned string occupies and
+ * is responsible for releasing it.
+ */
+char *fsmonitor__resolve_alias(const char *path,
+	const struct alias_info *info);
+
 #endif
Signature · 1 line
gitgitgadget
#4 lore raw

PATCH 3/12 fsmonitor: avoid socket location check if using hook

In reply to Eric DeCosta via GitGitGadget

From: Eric DeCosta <[email protected]>

If monitoring is done via fsmonitor hook rather than IPC there is no need to check if the location of the Unix Domain socket (UDS) file is on a remote filesystem.

---
 compat/fsmonitor/fsm-settings-darwin.c | 10 ++++++----
 compat/fsmonitor/fsm-settings-win32.c  |  2 +-
 fsmonitor-settings.c                   |  8 ++++----
 fsmonitor-settings.h                   |  2 +-
 4 files changed, 12 insertions(+), 10 deletions(-)
compat/fsmonitor/fsm-settings-darwin.c +6 -4
index 7241c4c22c9..6abbc7af3ab 100644
@@ -48,13 +48,15 @@ static enum fsmonitor_reason check_uds_volume(struct repository *r)
 	return FSMONITOR_REASON_OK;
 }
 
-enum fsmonitor_reason fsm_os__incompatible(struct repository *r)
+enum fsmonitor_reason fsm_os__incompatible(struct repository *r, int ipc)
 {
 	enum fsmonitor_reason reason;
 
-	reason = check_uds_volume(r);
-	if (reason != FSMONITOR_REASON_OK)
-		return reason;
+	if (ipc) {
+		reason = check_uds_volume(r);
+		if (reason != FSMONITOR_REASON_OK)
+			return reason;
+	}
 
 	return FSMONITOR_REASON_OK;
 }
compat/fsmonitor/fsm-settings-win32.c +1 -1
index d88b06ae610..a8af31b71de 100644
@@ -25,7 +25,7 @@ static enum fsmonitor_reason check_vfs4git(struct repository *r)
 	return FSMONITOR_REASON_OK;
 }
 
-enum fsmonitor_reason fsm_os__incompatible(struct repository *r)
+enum fsmonitor_reason fsm_os__incompatible(struct repository *r, int ipc)
 {
 	enum fsmonitor_reason reason;
 
fsmonitor-settings.c +4 -4
index d288cbad479..531a1b6f956 100644
@@ -60,7 +60,7 @@ static enum fsmonitor_reason check_remote(struct repository *r)
 }
 #endif
 
-static enum fsmonitor_reason check_for_incompatible(struct repository *r)
+static enum fsmonitor_reason check_for_incompatible(struct repository *r, int ipc)
 {
 	if (!r->worktree) {
 		/*
@@ -77,7 +77,7 @@ static enum fsmonitor_reason check_for_incompatible(struct repository *r)
 		reason = check_remote(r);
 		if (reason != FSMONITOR_REASON_OK)
 			return reason;
-		reason = fsm_os__incompatible(r);
+		reason = fsm_os__incompatible(r, ipc);
 		if (reason != FSMONITOR_REASON_OK)
 			return reason;
 	}
@@ -162,7 +162,7 @@ const char *fsm_settings__get_hook_path(struct repository *r)
 
 void fsm_settings__set_ipc(struct repository *r)
 {
-	enum fsmonitor_reason reason = check_for_incompatible(r);
+	enum fsmonitor_reason reason = check_for_incompatible(r, 1);
 
 	if (reason != FSMONITOR_REASON_OK) {
 		fsm_settings__set_incompatible(r, reason);
@@ -185,7 +185,7 @@ void fsm_settings__set_ipc(struct repository *r)
 
 void fsm_settings__set_hook(struct repository *r, const char *path)
 {
-	enum fsmonitor_reason reason = check_for_incompatible(r);
+	enum fsmonitor_reason reason = check_for_incompatible(r, 0);
 
 	if (reason != FSMONITOR_REASON_OK) {
 		fsm_settings__set_incompatible(r, reason);
fsmonitor-settings.h +1 -1
index d9c2605197f..0721617b95a 100644
@@ -48,7 +48,7 @@ struct fsmonitor_settings;
  * fsm_os__* routines should considered private to fsm_settings__
  * routines.
  */
-enum fsmonitor_reason fsm_os__incompatible(struct repository *r);
+enum fsmonitor_reason fsm_os__incompatible(struct repository *r, int ipc);
 #endif /* HAVE_FSMONITOR_OS_SETTINGS */
 
 #endif /* FSMONITOR_SETTINGS_H */
Signature · 1 line
gitgitgadget
#6 lore raw

PATCH 5/12 fsmonitor: check for compatability before communicating with fsmonitor

In reply to Eric DeCosta via GitGitGadget

From: Eric DeCosta <[email protected]>

If fsmonitor is not in a compatible state, warn with an appropriate message.

---
 fsmonitor-settings.c | 10 +++++++---
 fsmonitor-settings.h |  2 +-
 fsmonitor.c          |  7 +++++++
 3 files changed, 15 insertions(+), 4 deletions(-)
fsmonitor-settings.c +7 -3
index 531a1b6f956..ee63a97dc51 100644
@@ -1,6 +1,7 @@
 #include "cache.h"
 #include "config.h"
 #include "repository.h"
+#include "fsmonitor-ipc.h"
 #include "fsmonitor-settings.h"
 #include "fsmonitor-path-utils.h"
 
@@ -242,10 +243,11 @@ enum fsmonitor_reason fsm_settings__get_reason(struct repository *r)
 	return r->settings.fsmonitor->reason;
 }
 
-char *fsm_settings__get_incompatible_msg(const struct repository *r,
+char *fsm_settings__get_incompatible_msg(struct repository *r,
 					 enum fsmonitor_reason reason)
 {
 	struct strbuf msg = STRBUF_INIT;
+	const char *socket_dir;
 
 	switch (reason) {
 	case FSMONITOR_REASON_UNTESTED:
@@ -281,9 +283,11 @@ char *fsm_settings__get_incompatible_msg(const struct repository *r,
 		goto done;
 
 	case FSMONITOR_REASON_NOSOCKETS:
+		socket_dir = dirname((char *)fsmonitor_ipc__get_path(r));
 		strbuf_addf(&msg,
-			    _("repository '%s' is incompatible with fsmonitor due to lack of Unix sockets"),
-			    r->worktree);
+			    _("socket directory '%s' is incompatible with fsmonitor due"
+			      " to lack of Unix sockets support"),
+			    socket_dir);
 		goto done;
 	}
 
fsmonitor-settings.h +1 -1
index 0721617b95a..ab02e3995ee 100644
@@ -33,7 +33,7 @@ enum fsmonitor_mode fsm_settings__get_mode(struct repository *r);
 const char *fsm_settings__get_hook_path(struct repository *r);
 
 enum fsmonitor_reason fsm_settings__get_reason(struct repository *r);
-char *fsm_settings__get_incompatible_msg(const struct repository *r,
+char *fsm_settings__get_incompatible_msg(struct repository *r,
 					 enum fsmonitor_reason reason);
 
 struct fsmonitor_settings;
fsmonitor.c +7 -0
index 57d6a483bee..540736b39fd 100644
@@ -295,6 +295,7 @@ static int fsmonitor_force_update_threshold = 100;
 
 void refresh_fsmonitor(struct index_state *istate)
 {
+	static int warn_once = 0;
 	struct strbuf query_result = STRBUF_INIT;
 	int query_success = 0, hook_version = -1;
 	size_t bol = 0; /* beginning of line */
@@ -305,6 +306,12 @@ void refresh_fsmonitor(struct index_state *istate)
 	int is_trivial = 0;
 	struct repository *r = istate->repo ? istate->repo : the_repository;
 	enum fsmonitor_mode fsm_mode = fsm_settings__get_mode(r);
+	enum fsmonitor_reason reason = fsm_settings__get_reason(r);
+
+	if (!warn_once && reason > FSMONITOR_REASON_OK) {
+		warn_once = 1;
+		warning("%s", fsm_settings__get_incompatible_msg(r, reason));
+	}
 
 	if (fsm_mode <= FSMONITOR_MODE_DISABLED ||
 	    istate->fsmonitor_has_run_once)
Signature · 1 line
gitgitgadget
#1 lore raw

PATCH 1/12 fsmonitor: refactor filesystem checks to common interface

In reply to Eric DeCosta via GitGitGadget

From: Eric DeCosta <[email protected]>

Provide a common interface for getting basic filesystem information including filesystem type and whether the filesystem is remote.

Refactor existing code for getting basic filesystem info and detecting remote file systems to the new interface.

Refactor filesystem checks to leverage new interface. For macOS, error-out if the Unix Domain socket (UDS) file is on a remote filesystem.

---
 Makefile                                 |   1 +
 compat/fsmonitor/fsm-path-utils-darwin.c |  43 ++++++
 compat/fsmonitor/fsm-path-utils-win32.c  | 128 +++++++++++++++++
 compat/fsmonitor/fsm-settings-darwin.c   |  69 +++------
 compat/fsmonitor/fsm-settings-win32.c    | 172 +----------------------
 contrib/buildsystems/CMakeLists.txt      |   2 +
 fsmonitor-path-utils.h                   |  26 ++++
 fsmonitor-settings.c                     |  50 +++++++
 8 files changed, 272 insertions(+), 219 deletions(-)
 create mode 100644 compat/fsmonitor/fsm-path-utils-darwin.c
 create mode 100644 compat/fsmonitor/fsm-path-utils-win32.c
 create mode 100644 fsmonitor-path-utils.h
Makefile +1 -0
index cac3452edb9..ffab427ea5b 100644
@@ -2044,6 +2044,7 @@ endif
 ifdef FSMONITOR_OS_SETTINGS
 	COMPAT_CFLAGS += -DHAVE_FSMONITOR_OS_SETTINGS
 	COMPAT_OBJS += compat/fsmonitor/fsm-settings-$(FSMONITOR_OS_SETTINGS).o
+	COMPAT_OBJS += compat/fsmonitor/fsm-path-utils-$(FSMONITOR_OS_SETTINGS).o
 endif
 
 ifeq ($(TCLTK_PATH),)
compat/fsmonitor/fsm-path-utils-darwin.c new file +43 -0

43 lines not on this page · raw message

compat/fsmonitor/fsm-path-utils-win32.c new file +128 -0

128 lines not on this page · raw message

compat/fsmonitor/fsm-settings-darwin.c +20 -49

101 lines not on this page · raw message

compat/fsmonitor/fsm-settings-win32.c +2 -170

190 lines not on this page · raw message

contrib/buildsystems/CMakeLists.txt +2 -0
index ea2a531be87..5482a04b3ce 100644
@@ -308,6 +308,7 @@ if(SUPPORTS_SIMPLE_IPC)
 		add_compile_definitions(HAVE_FSMONITOR_DAEMON_BACKEND)
 		list(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-win32.c)
 		list(APPEND compat_SOURCES compat/fsmonitor/fsm-health-win32.c)
+		list(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-win32.c)
 
 		add_compile_definitions(HAVE_FSMONITOR_OS_SETTINGS)
 		list(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-win32.c)
@@ -315,6 +316,7 @@ if(SUPPORTS_SIMPLE_IPC)
 		add_compile_definitions(HAVE_FSMONITOR_DAEMON_BACKEND)
 		list(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-darwin.c)
 		list(APPEND compat_SOURCES compat/fsmonitor/fsm-health-darwin.c)
+		list(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-darwin.c)
 
 		add_compile_definitions(HAVE_FSMONITOR_OS_SETTINGS)
 		list(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-darwin.c)
fsmonitor-path-utils.h new file +26 -0
new file mode 100644
index 00000000000..41edf5b934f
@@ -0,0 +1,26 @@
+#ifndef FSM_PATH_UTILS_H
+#define FSM_PATH_UTILS_H
+
+struct fs_info {
+	int is_remote;
+	char *typename;
+};
+
+/*
+ * Get some basic filesystem informtion for the given path
+ *
+ * The caller owns the storage that is occupied by fs_info and
+ * is responsible for releasing it.
+ *
+ * Returns -1 on error, zero otherwise.
+ */
+int fsmonitor__get_fs_info(const char *path, struct fs_info *fs_info);
+
+/*
+ * Determines if the filesystem that path resides on is remote.
+ *
+ * Returns -1 on error, 0 if not remote, 1 if remote.
+ */
+int fsmonitor__is_fs_remote(const char *path);
+
+#endif
fsmonitor-settings.c +50 -0

68 lines not on this page · raw message

Signature · 1 line
gitgitgadget
#7 lore raw

PATCH 6/12 fsmonitor: add documentation for allowRemote and socketDir options

In reply to Eric DeCosta via GitGitGadget

From: Eric DeCosta <[email protected]>

Add documentation for 'fsmonitor.allowRemote' and 'fsmonitor.socketDir'. Call-out experimental nature of 'fsmonitor.allowRemote' and limited filesystem support for 'fsmonitor.socketDir'.

---
 Documentation/config.txt                   |  2 ++
 Documentation/config/fsmonitor--daemon.txt | 11 +++++++
 Documentation/git-fsmonitor--daemon.txt    | 37 ++++++++++++++++++++--
 3 files changed, 47 insertions(+), 3 deletions(-)
 create mode 100644 Documentation/config/fsmonitor--daemon.txt
Documentation/config.txt +2 -0
index 5b5b9765699..1e205831656 100644
@@ -423,6 +423,8 @@ include::config/filter.txt[]
 
 include::config/fsck.txt[]
 
+include::config/fsmonitor--daemon.txt[]
+
 include::config/gc.txt[]
 
 include::config/gitcvs.txt[]
Documentation/config/fsmonitor--daemon.txt new file +11 -0
new file mode 100644
index 00000000000..c225c6c9e74
@@ -0,0 +1,11 @@
+fsmonitor.allowRemote::
+    By default, the fsmonitor daemon refuses to work against network-mounted
+    repositories. Setting `fsmonitor.allowRemote` to `true` overrides this
+    behavior.  Only respected when `core.fsmonitor` is set to `true`.
+
+fsmonitor.socketDir::
+    This Mac OS-specific option, if set, specifies the directory in
+    which to create the Unix domain socket used for communication
+    between the fsmonitor daemon and various Git commands. The directory must
+    reside on a native Mac OS filesystem.  Only respected when `core.fsmonitor`
+    is set to `true`.
Documentation/git-fsmonitor--daemon.txt +34 -3
index cc142fb8612..8238eadb0e1 100644
@@ -3,7 +3,7 @@ git-fsmonitor{litdd}daemon(1)
 
 NAME
 ----
-git-fsmonitor--daemon - A Built-in File System Monitor
+git-fsmonitor--daemon - A Built-in Filesystem Monitor
 
 SYNOPSIS
 --------
@@ -17,7 +17,7 @@ DESCRIPTION
 -----------
 
 A daemon to watch the working directory for file and directory
-changes using platform-specific file system notification facilities.
+changes using platform-specific filesystem notification facilities.
 
 This daemon communicates directly with commands like `git status`
 using the link:technical/api-simple-ipc.html[simple IPC] interface
@@ -63,13 +63,44 @@ CAVEATS
 -------
 
 The fsmonitor daemon does not currently know about submodules and does
-not know to filter out file system events that happen within a
+not know to filter out filesystem 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
 will properly ignore these extra events, so performance may be affected
 but it will not cause an incorrect result.
 
+By default, the fsmonitor daemon refuses to work against network-mounted
+repositories; this may be overridden by setting `fsmonitor.allowRemote` to
+`true`. Note, however, that the fsmonitor daemon is not guaranteed to work
+correctly with all network-mounted repositories and such use is considered
+experimental.
+
+On Mac OS, the inter-process communication (IPC) between various Git
+commands and the fsmonitor daemon is done via a Unix domain socket (UDS) -- a
+special type of file -- which is supported by native Mac OS filesystems,
+but not on network-mounted filesystems, NTFS, or FAT32.  Other filesystems
+may or may not have the needed support; the fsmonitor daemon is not guaranteed
+to work with these filesystems and such use is considered experimental.
+
+By default, the socket is created in the `.git` directory, however, if the
+`.git` directory is on a network-mounted filesystem, it will be instead be
+created at `$HOME/.git-fsmonitor-*` unless `$HOME` itself is on a
+network-mounted filesystem in which case you must set the configuration
+variable `fsmonitor.socketDir` to the path of a directory on a Mac OS native
+filesystem in which to create the socket file.
+
+If none of the above directories (`.git`, `$HOME`, or `fsmonitor.socketDir`)
+is on a native Mac OS file filesystem the fsmonitor daemon will report an
+error that will cause the daemon and the currently running command to exit.
+
+CONFIGURATION
+-------------
+
+include::includes/cmd-config-section-all.txt[]
+
+include::config/fsmonitor--daemon.txt[]
+
 GIT
 ---
 Part of the linkgit:git[1] suite
Signature · 1 line
gitgitgadget
#8 lore raw

PATCH 7/12 fsmonitor: prepare to share code between Mac OS and Linux

In reply to Eric DeCosta via GitGitGadget

From: Eric DeCosta <[email protected]>

Linux and Mac OS can share some of the code originally developed for Mac OS. Rename the shared code from compat/fsmonitor/fsm-*-dawrin.c to compat/fsmonitor/fsm-*-unix.c

Update the build to enable sharing of the fsm-*-unix.c files.

Minor update to compat/fsmonitor/fsm-ipc-unix.c to make it cross-platform.

---
 Makefile                                                  | 6 +++---
 .../fsmonitor/{fsm-health-darwin.c => fsm-health-unix.c}  | 0
 compat/fsmonitor/{fsm-ipc-darwin.c => fsm-ipc-unix.c}     | 8 ++++----
 .../{fsm-settings-darwin.c => fsm-settings-unix.c}        | 0
 config.mak.uname                                          | 4 ++++
 contrib/buildsystems/CMakeLists.txt                       | 6 +++---
 6 files changed, 14 insertions(+), 10 deletions(-)
 rename compat/fsmonitor/{fsm-health-darwin.c => fsm-health-unix.c} (100%)
 rename compat/fsmonitor/{fsm-ipc-darwin.c => fsm-ipc-unix.c} (89%)
 rename compat/fsmonitor/{fsm-settings-darwin.c => fsm-settings-unix.c} (100%)
Makefile +3 -3
index feb675a6959..31dd6ab2734 100644
@@ -2038,13 +2038,13 @@ endif
 ifdef FSMONITOR_DAEMON_BACKEND
 	COMPAT_CFLAGS += -DHAVE_FSMONITOR_DAEMON_BACKEND
 	COMPAT_OBJS += compat/fsmonitor/fsm-listen-$(FSMONITOR_DAEMON_BACKEND).o
-	COMPAT_OBJS += compat/fsmonitor/fsm-health-$(FSMONITOR_DAEMON_BACKEND).o
-	COMPAT_OBJS += compat/fsmonitor/fsm-ipc-$(FSMONITOR_DAEMON_BACKEND).o
+	COMPAT_OBJS += compat/fsmonitor/fsm-health-$(FSMONITOR_DAEMON_COMMON).o
+	COMPAT_OBJS += compat/fsmonitor/fsm-ipc-$(FSMONITOR_DAEMON_COMMON).o
 endif
 
 ifdef FSMONITOR_OS_SETTINGS
 	COMPAT_CFLAGS += -DHAVE_FSMONITOR_OS_SETTINGS
-	COMPAT_OBJS += compat/fsmonitor/fsm-settings-$(FSMONITOR_OS_SETTINGS).o
+	COMPAT_OBJS += compat/fsmonitor/fsm-settings-$(FSMONITOR_DAEMON_COMMON).o
 	COMPAT_OBJS += compat/fsmonitor/fsm-path-utils-$(FSMONITOR_OS_SETTINGS).o
 endif
 
compat/fsmonitor/fsm-health-unix.c renamed from compat/fsmonitor/fsm-health-darwin.c +0 -0
similarity index 100%
rename from compat/fsmonitor/fsm-health-darwin.c
rename to compat/fsmonitor/fsm-health-unix.c
compat/fsmonitor/fsm-ipc-unix.c renamed from compat/fsmonitor/fsm-ipc-darwin.c +4 -4
similarity index 89%
rename from compat/fsmonitor/fsm-ipc-darwin.c
rename to compat/fsmonitor/fsm-ipc-unix.c
index ce843d63348..3ba3b9e17ed 100644
@@ -10,7 +10,7 @@ static GIT_PATH_FUNC(fsmonitor_ipc__get_default_path, "fsmonitor--daemon.ipc")
 const char *fsmonitor_ipc__get_path(struct repository *r)
 {
 	static const char *ipc_path = NULL;
-	SHA_CTX sha1ctx;
+	git_SHA_CTX sha1ctx;
 	char *sock_dir = NULL;
 	struct strbuf ipc_file = STRBUF_INIT;
 	unsigned char hash[SHA_DIGEST_LENGTH];
@@ -28,9 +28,9 @@ const char *fsmonitor_ipc__get_path(struct repository *r)
 		return ipc_path;
 	}
 
-	SHA1_Init(&sha1ctx);
-	SHA1_Update(&sha1ctx, r->worktree, strlen(r->worktree));
-	SHA1_Final(hash, &sha1ctx);
+	git_SHA1_Init(&sha1ctx);
+	git_SHA1_Update(&sha1ctx, r->worktree, strlen(r->worktree));
+	git_SHA1_Final(hash, &sha1ctx);
 
 	repo_config_get_string(r, "fsmonitor.socketdir", &sock_dir);
 
compat/fsmonitor/fsm-settings-unix.c renamed from compat/fsmonitor/fsm-settings-darwin.c +0 -0
similarity index 100%
rename from compat/fsmonitor/fsm-settings-darwin.c
rename to compat/fsmonitor/fsm-settings-unix.c
config.mak.uname +4 -0
index d63629fe807..d454cec47c4 100644
@@ -68,6 +68,7 @@ ifeq ($(uname_S),Linux)
 	ifneq ($(findstring .el7.,$(uname_R)),)
 		BASIC_CFLAGS += -std=c99
 	endif
+
 endif
 ifeq ($(uname_S),GNU/kFreeBSD)
 	HAVE_ALLOCA_H = YesPlease
@@ -165,6 +166,7 @@ ifeq ($(uname_S),Darwin)
 	ifndef NO_UNIX_SOCKETS
 	FSMONITOR_DAEMON_BACKEND = darwin
 	FSMONITOR_OS_SETTINGS = darwin
+	FSMONITOR_DAEMON_COMMON = unix
 	endif
 	endif
 
@@ -453,6 +455,7 @@ ifeq ($(uname_S),Windows)
 	# support it.
 	FSMONITOR_DAEMON_BACKEND = win32
 	FSMONITOR_OS_SETTINGS = win32
+	FSMONITOR_DAEMON_COMMON = win32
 
 	NO_SVN_TESTS = YesPlease
 	RUNTIME_PREFIX = YesPlease
@@ -645,6 +648,7 @@ ifeq ($(uname_S),MINGW)
 	# support it.
 	FSMONITOR_DAEMON_BACKEND = win32
 	FSMONITOR_OS_SETTINGS = win32
+	FSMONITOR_DAEMON_COMMON = win32
 
 	RUNTIME_PREFIX = YesPlease
 	HAVE_WPGMPTR = YesWeDo
contrib/buildsystems/CMakeLists.txt +3 -3
index 787738e6fa3..0b26a1a36e3 100644
@@ -315,13 +315,13 @@ if(SUPPORTS_SIMPLE_IPC)
 		list(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-win32.c)
 	elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
 		add_compile_definitions(HAVE_FSMONITOR_DAEMON_BACKEND)
+		list(APPEND compat_SOURCES compat/fsmonitor/fsm-ipc-unix.c)
+		list(APPEND compat_SOURCES compat/fsmonitor/fsm-health-unix.c)
 		list(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-darwin.c)
-		list(APPEND compat_SOURCES compat/fsmonitor/fsm-health-darwin.c)
-		list(APPEND compat_SOURCES compat/fsmonitor/fsm-ipc-darwin.c)
 		list(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-darwin.c)
 
 		add_compile_definitions(HAVE_FSMONITOR_OS_SETTINGS)
-		list(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-darwin.c)
+		list(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-unix.c)
 	endif()
 endif()
 
Signature · 1 line
gitgitgadget
#11 lore raw

PATCH 8/12 fsmonitor: determine if filesystem is local or remote

In reply to Eric DeCosta via GitGitGadget

From: Eric DeCosta <[email protected]>

Compare the given path to the mounted filesystems. Find the mount that is the longest prefix of the path (if any) and determine if that mount is on a local or remote filesystem.

---
 compat/fsmonitor/fsm-path-utils-linux.c | 163 ++++++++++++++++++++++++
 1 file changed, 163 insertions(+)
 create mode 100644 compat/fsmonitor/fsm-path-utils-linux.c
compat/fsmonitor/fsm-path-utils-linux.c new file +163 -0
new file mode 100644
index 00000000000..369692a788f
@@ -0,0 +1,163 @@
+#include "fsmonitor.h"
+#include "fsmonitor-path-utils.h"
+#include <errno.h>
+#include <mntent.h>
+#include <sys/mount.h>
+#include <sys/statvfs.h>
+
+/*
+ * https://github.com/coreutils/gnulib/blob/master/lib/mountlist.c
+ */
+#ifndef ME_REMOTE
+/* A file system is "remote" if its Fs_name contains a ':'
+   or if (it is of type (smbfs or cifs) and its Fs_name starts with '//')
+   or if it is of any other of the listed types
+   or Fs_name is equal to "-hosts" (used by autofs to mount remote fs).
+   "VM" file systems like prl_fs or vboxsf are not considered remote here. */
+# define ME_REMOTE(Fs_name, Fs_type)            \
+	(strchr (Fs_name, ':') != NULL              \
+	 || ((Fs_name)[0] == '/'                    \
+		 && (Fs_name)[1] == '/'                 \
+		 && (strcmp (Fs_type, "smbfs") == 0     \
+			 || strcmp (Fs_type, "smb3") == 0   \
+			 || strcmp (Fs_type, "cifs") == 0)) \
+	 || strcmp (Fs_type, "acfs") == 0           \
+	 || strcmp (Fs_type, "afs") == 0            \
+	 || strcmp (Fs_type, "coda") == 0           \
+	 || strcmp (Fs_type, "auristorfs") == 0     \
+	 || strcmp (Fs_type, "fhgfs") == 0          \
+	 || strcmp (Fs_type, "gpfs") == 0           \
+	 || strcmp (Fs_type, "ibrix") == 0          \
+	 || strcmp (Fs_type, "ocfs2") == 0          \
+	 || strcmp (Fs_type, "vxfs") == 0           \
+	 || strcmp ("-hosts", Fs_name) == 0)
+#endif
+
+static struct mntent *find_mount(const char *path, const struct statvfs *fs)
+{
+	const char *const mounts = "/proc/mounts";
+	const char *rp = real_pathdup(path, 1);
+	struct mntent *ment = NULL;
+	struct mntent *found = NULL;
+	struct statvfs mntfs;
+	FILE *fp;
+	int dlen, plen, flen = 0;
+
+	fp = setmntent(mounts, "r");
+	if (!fp) {
+		error_errno(_("setmntent('%s') failed"), mounts);
+		return NULL;
+	}
+
+	plen = strlen(rp);
+
+	/* read all the mount information and compare to path */
+	while ((ment = getmntent(fp)) != NULL) {
+		if (statvfs(ment->mnt_dir, &mntfs)) {
+			switch (errno) {
+			case EPERM:
+			case ESRCH:
+			case EACCES:
+				continue;
+			default:
+				error_errno(_("statvfs('%s') failed"), ment->mnt_dir);
+				endmntent(fp);
+				return NULL;
+			}
+		}
+
+		/* is mount on the same filesystem and is a prefix of the path */
+		if ((fs->f_fsid == mntfs.f_fsid) &&
+			!strncmp(ment->mnt_dir, rp, strlen(ment->mnt_dir))) {
+			dlen = strlen(ment->mnt_dir);
+			if (dlen > plen)
+				continue;
+			/*
+			 * root is always a potential match; otherwise look for
+			 * directory prefix
+			 */
+			if ((dlen == 1 && ment->mnt_dir[0] == '/') ||
+				(dlen > flen && (!rp[dlen] || rp[dlen] == '/'))) {
+				flen = dlen;
+				/*
+				 * https://man7.org/linux/man-pages/man3/getmntent.3.html
+				 *
+				 * The pointer points to a static area of memory which is
+				 * overwritten by subsequent calls to getmntent().
+				 */
+				if (!found)
+					CALLOC_ARRAY(found, 1);
+				free(found->mnt_dir);
+				free(found->mnt_type);
+				free(found->mnt_fsname);
+				found->mnt_dir = xmemdupz(ment->mnt_dir, strlen(ment->mnt_dir));
+				found->mnt_type = xmemdupz(ment->mnt_type, strlen(ment->mnt_type));
+				found->mnt_fsname = xmemdupz(ment->mnt_fsname, strlen(ment->mnt_fsname));
+			}
+		}
+	}
+	endmntent(fp);
+
+	return found;
+}
+
+int fsmonitor__get_fs_info(const char *path, struct fs_info *fs_info)
+{
+	struct mntent *ment;
+	struct statvfs fs;
+
+	if (statvfs(path, &fs))
+		return error_errno(_("statvfs('%s') failed"), path);
+
+	ment = find_mount(path, &fs);
+	if (!ment)
+		return -1;
+
+	trace_printf_key(&trace_fsmonitor,
+			 "statvfs('%s') [flags 0x%08lx] '%s' '%s'",
+			 path, fs.f_flag, ment->mnt_type, ment->mnt_fsname);
+
+	if (ME_REMOTE(ment->mnt_fsname, ment->mnt_type))
+		fs_info->is_remote = 1;
+	else
+		fs_info->is_remote = 0;
+
+	fs_info->typename = ment->mnt_fsname;
+	free(ment->mnt_dir);
+	free(ment->mnt_type);
+	free(ment);
+
+	trace_printf_key(&trace_fsmonitor,
+				"'%s' is_remote: %d",
+				path, fs_info->is_remote);
+	return 0;
+}
+
+int fsmonitor__is_fs_remote(const char *path)
+{
+	struct fs_info fs;
+
+	if (fsmonitor__get_fs_info(path, &fs))
+		return -1;
+
+	free(fs.typename);
+
+	return fs.is_remote;
+}
+
+/*
+ * No-op for now.
+ */
+int fsmonitor__get_alias(const char *path, struct alias_info *info)
+{
+	return 0;
+}
+
+/*
+ * No-op for now.
+ */
+char *fsmonitor__resolve_alias(const char *path,
+	const struct alias_info *info)
+{
+	return NULL;
+}
Signature · 1 line
gitgitgadget
#14 lore raw

PATCH 9/12 fsmonitor: implement filesystem change listener for Linux

In reply to Eric DeCosta via GitGitGadget

From: Eric DeCosta <[email protected]>

Implement a filesystem change listener for Linux based on the inotify API:
https://man7.org/linux/man-pages/man7/inotify.7.html

inotify requires registering a watch on every directory in the worktree and special handling of moves/renames.

---
 compat/fsmonitor/fsm-listen-linux.c | 664 ++++++++++++++++++++++++++++
 1 file changed, 664 insertions(+)
 create mode 100644 compat/fsmonitor/fsm-listen-linux.c
compat/fsmonitor/fsm-listen-linux.c new file +664 -0
new file mode 100644
index 00000000000..244832fbec7
@@ -0,0 +1,664 @@
+#include "cache.h"
+#include "fsmonitor.h"
+#include "fsm-listen.h"
+#include "fsmonitor--daemon.h"
+#include <dirent.h>
+#include <fcntl.h>
+#include <sys/inotify.h>
+#include <sys/stat.h>
+
+/*
+ * Safe value to bitwise OR with rest of mask for
+ * kernels that do not support IN_MASK_CREATE
+ */
+#ifndef IN_MASK_CREATE
+#define IN_MASK_CREATE 0x00000000
+#endif
+
+enum shutdown_reason {
+	SHUTDOWN_CONTINUE = 0,
+	SHUTDOWN_STOP,
+	SHUTDOWN_ERROR,
+	SHUTDOWN_FORCE
+};
+
+struct watch_entry {
+	struct hashmap_entry ent;
+	int wd;
+	uint32_t cookie;
+	const char *dir;
+};
+
+struct rename_entry {
+	struct hashmap_entry ent;
+	time_t whence;
+	uint32_t cookie;
+	const char *dir;
+};
+
+struct fsm_listen_data {
+	int fd_inotify;
+	enum shutdown_reason shutdown;
+	struct hashmap watches;
+	struct hashmap renames;
+	struct hashmap revwatches;
+};
+
+static int watch_entry_cmp(const void *cmp_data,
+			  const struct hashmap_entry *eptr,
+			  const struct hashmap_entry *entry_or_key,
+			  const void *keydata)
+{
+	const struct watch_entry *e1, *e2;
+
+	e1 = container_of(eptr, const struct watch_entry, ent);
+	e2 = container_of(eptr, const struct watch_entry, ent);
+	return e1->wd != e2->wd;
+}
+
+static int revwatches_entry_cmp(const void *cmp_data,
+			  const struct hashmap_entry *eptr,
+			  const struct hashmap_entry *entry_or_key,
+			  const void *keydata)
+{
+	const struct watch_entry *e1, *e2;
+
+	e1 = container_of(eptr, const struct watch_entry, ent);
+	e2 = container_of(eptr, const struct watch_entry, ent);
+	return strcmp(e1->dir, e2->dir);
+}
+
+static int rename_entry_cmp(const void *cmp_data,
+			  const struct hashmap_entry *eptr,
+			  const struct hashmap_entry *entry_or_key,
+			  const void *keydata)
+{
+	const struct rename_entry *e1, *e2;
+
+	e1 = container_of(eptr, const struct rename_entry, ent);
+	e2 = container_of(eptr, const struct rename_entry, ent);
+	return e1->cookie != e2->cookie;
+}
+
+/*
+ * Register an inotify watch, add watch descriptor to path mapping
+ * and the reverse mapping.
+ */
+static int add_watch(const char *path, struct fsm_listen_data *data)
+{
+	const char *interned = strintern(path);
+	struct watch_entry *w1, *w2;
+
+	/* add the inotify watch, don't allow watches to be modified */
+	int wd = inotify_add_watch(data->fd_inotify, interned,
+				(IN_ALL_EVENTS | IN_ONLYDIR | IN_MASK_CREATE)
+				^ IN_ACCESS ^ IN_CLOSE ^ IN_OPEN);
+	if (wd < 0)
+		return error_errno("inotify_add_watch('%s') failed", interned);
+
+	/* add watch descriptor -> directory mapping */
+	CALLOC_ARRAY(w1, 1);
+	w1->wd = wd;
+	w1->dir = interned;
+	hashmap_entry_init(&w1->ent, memhash(&w1->wd, sizeof(int)));
+	hashmap_add(&data->watches, &w1->ent);
+
+	/* add directory -> watch descriptor mapping */
+	CALLOC_ARRAY(w2, 1);
+	w2->wd = wd;
+	w2->dir = interned;
+	hashmap_entry_init(&w2->ent, memhash(w2->dir, strlen(w2->dir)));
+	hashmap_add(&data->revwatches, &w2->ent);
+
+	return 0;
+}
+
+/*
+ * Remove the inotify watch, the watch descriptor to path mapping
+ * and the reverse mapping.
+ */
+static void remove_watch(struct watch_entry *w,
+	struct fsm_listen_data *data)
+{
+	struct watch_entry k1, k2, *w1, *w2;
+
+	/* remove watch, ignore error if kernel already did it */
+	if (inotify_rm_watch(data->fd_inotify, w->wd) && errno != EINVAL)
+		error_errno("inotify_rm_watch() failed");
+
+	hashmap_entry_init(&k1.ent, memhash(&w->wd, sizeof(int)));
+	w1 = hashmap_remove_entry(&data->watches, &k1, ent, NULL);
+	if (!w1)
+		BUG("Double remove of watch for '%s'", w->dir);
+
+	if (w1->cookie)
+		BUG("Removing watch for '%s' which has a pending rename", w1->dir);
+
+	hashmap_entry_init(&k2.ent, memhash(w->dir, strlen(w->dir)));
+	w2 = hashmap_remove_entry(&data->revwatches, &k2, ent, NULL);
+	if (!w2)
+		BUG("Double remove of reverse watch for '%s'", w->dir);
+
+	/* w1->dir and w2->dir are interned strings, we don't own them */
+	free(w1);
+	free(w2);
+}
+
+/*
+ * Check for stale directory renames.
+ *
+ * https://man7.org/linux/man-pages/man7/inotify.7.html
+ *
+ * Allow for some small timeout to account for the fact that insertion of the
+ * IN_MOVED_FROM+IN_MOVED_TO event pair is not atomic, and the possibility that
+ * there may not be any IN_MOVED_TO event.
+ *
+ * If the IN_MOVED_TO event is not received within the timeout then events have
+ * been missed and the monitor is in an inconsistent state with respect to the
+ * filesystem.
+ */
+static int check_stale_dir_renames(struct hashmap *renames, time_t max_age)
+{
+	struct rename_entry *re;
+	struct hashmap_iter iter;
+
+	hashmap_for_each_entry(renames, &iter, re, ent) {
+		if (re->whence <= max_age)
+			return -1;
+	}
+	return 0;
+}
+
+/*
+ * Track pending renames.
+ *
+ * Tracking is done via a event cookie to watch descriptor mapping.
+ *
+ * A rename is not complete until matching a IN_MOVED_TO event is received
+ * for a corresponding IN_MOVED_FROM event.
+ */
+static void add_dir_rename(uint32_t cookie, const char *path,
+	struct fsm_listen_data *data)
+{
+	struct watch_entry k, *w;
+	struct rename_entry *re;
+
+	/* lookup the watch descriptor for the given path */
+	hashmap_entry_init(&k.ent, memhash(path, strlen(path)));
+	w = hashmap_get_entry(&data->revwatches, &k, ent, NULL);
+	if (!w) /* should never happen */
+		BUG("No watch for '%s'", path);
+	w->cookie = cookie;
+
+	/* add the pending rename to match against later */
+	CALLOC_ARRAY(re, 1);
+	re->dir = w->dir;
+	re->cookie = w->cookie;
+	re->whence = time(NULL);
+	hashmap_entry_init(&re->ent, memhash(&re->cookie, sizeof(uint32_t)));
+	hashmap_add(&data->renames, &re->ent);
+}
+
+/*
+ * Handle directory renames
+ *
+ * Once a IN_MOVED_TO event is received, lookup the rename tracking information
+ * via the event cookie and use this information to update the watch.
+ */
+static void rename_dir(uint32_t cookie, const char *path,
+	struct fsm_listen_data *data)
+{
+	struct rename_entry rek, *re;
+	struct watch_entry k, *w;
+
+	/* lookup a pending rename to match */
+	rek.cookie = cookie;
+	hashmap_entry_init(&rek.ent, memhash(&rek.cookie, sizeof(uint32_t)));
+	re = hashmap_get_entry(&data->renames, &rek, ent, NULL);
+	if (re) {
+		k.dir = re->dir;
+		hashmap_entry_init(&k.ent, memhash(k.dir, strlen(k.dir)));
+		w = hashmap_get_entry(&data->revwatches, &k, ent, NULL);
+		if (w) {
+			w->cookie = 0; /* rename handled */
+			remove_watch(w, data);
+			add_watch(path, data);
+		} else {
+			BUG("No matching watch");
+		}
+	} else {
+		BUG("No matching cookie");
+	}
+}
+
+/*
+ * Recursively add watches to every directory under path
+ */
+static int register_inotify(const char *path, struct fsm_listen_data *data)
+{
+	DIR *dir;
+	struct strbuf current = STRBUF_INIT;
+	struct dirent *de;
+	struct stat fs;
+	int ret = -1;
+
+	dir = opendir(path);
+	if (!dir)
+		return error_errno("opendir('%s') failed", path);
+
+	while ((de = readdir_skip_dot_and_dotdot(dir)) != NULL) {
+		strbuf_reset(&current);
+		strbuf_addf(&current, "%s/%s", path, de->d_name);
+		if (lstat(current.buf, &fs)) {
+			error_errno("lstat('%s') failed", current.buf);
+			goto failed;
+		}
+
+		/* recurse into directory */
+		if (S_ISDIR(fs.st_mode)) {
+			if (add_watch(current.buf, data))
+				goto failed;
+			if (register_inotify(current.buf, data))
+				goto failed;
+		}
+	}
+	ret = 0;
+
+failed:
+	strbuf_release(&current);
+	if (closedir(dir) < 0)
+		return error_errno("closedir('%s') failed", path);
+	return ret;
+}
+
+static int em_rename_dir_from(u_int32_t mask)
+{
+	return ((mask & IN_ISDIR) && (mask & IN_MOVED_FROM));
+}
+
+static int em_rename_dir_to(u_int32_t mask)
+{
+	return ((mask & IN_ISDIR) && (mask & IN_MOVED_TO));
+}
+
+static int em_remove_watch(u_int32_t mask)
+{
+	return (mask & IN_DELETE_SELF);
+}
+
+static int em_dir_renamed(u_int32_t mask)
+{
+	return ((mask & IN_ISDIR) && (mask & IN_MOVE));
+}
+
+static int em_dir_created(u_int32_t mask)
+{
+	return ((mask & IN_ISDIR) && (mask & IN_CREATE));
+}
+
+static int em_dir_deleted(uint32_t mask)
+{
+	return ((mask & IN_ISDIR) && (mask & IN_DELETE));
+}
+
+static int em_force_shutdown(u_int32_t mask)
+{
+	return (mask & IN_UNMOUNT) || (mask & IN_Q_OVERFLOW);
+}
+
+static int em_ignore(u_int32_t mask)
+{
+	return (mask & IN_IGNORED) || (mask & IN_MOVE_SELF);
+}
+
+static void log_mask_set(const char *path, u_int32_t mask)
+{
+	struct strbuf msg = STRBUF_INIT;
+
+	if (mask & IN_ACCESS)
+		strbuf_addstr(&msg, "IN_ACCESS|");
+	if (mask & IN_MODIFY)
+		strbuf_addstr(&msg, "IN_MODIFY|");
+	if (mask & IN_ATTRIB)
+		strbuf_addstr(&msg, "IN_ATTRIB|");
+	if (mask & IN_CLOSE_WRITE)
+		strbuf_addstr(&msg, "IN_CLOSE_WRITE|");
+	if (mask & IN_CLOSE_NOWRITE)
+		strbuf_addstr(&msg, "IN_CLOSE_NOWRITE|");
+	if (mask & IN_OPEN)
+		strbuf_addstr(&msg, "IN_OPEN|");
+	if (mask & IN_MOVED_FROM)
+		strbuf_addstr(&msg, "IN_MOVED_FROM|");
+	if (mask & IN_MOVED_TO)
+		strbuf_addstr(&msg, "IN_MOVED_TO|");
+	if (mask & IN_CREATE)
+		strbuf_addstr(&msg, "IN_CREATE|");
+	if (mask & IN_DELETE)
+		strbuf_addstr(&msg, "IN_DELETE|");
+	if (mask & IN_DELETE_SELF)
+		strbuf_addstr(&msg, "IN_DELETE_SELF|");
+	if (mask & IN_MOVE_SELF)
+		strbuf_addstr(&msg, "IN_MOVE_SELF|");
+	if (mask & IN_UNMOUNT)
+		strbuf_addstr(&msg, "IN_UNMOUNT|");
+	if (mask & IN_Q_OVERFLOW)
+		strbuf_addstr(&msg, "IN_Q_OVERFLOW|");
+	if (mask & IN_IGNORED)
+		strbuf_addstr(&msg, "IN_IGNORED|");
+	if (mask & IN_ISDIR)
+		strbuf_addstr(&msg, "IN_ISDIR|");
+
+	trace_printf_key(&trace_fsmonitor, "inotify_event: '%s', mask=%#8.8x %s",
+				path, mask, msg.buf);
+
+	strbuf_release(&msg);
+}
+
+int fsm_listen__ctor(struct fsmonitor_daemon_state *state)
+{
+	int fd;
+	int ret = 0;
+	struct fsm_listen_data *data;
+
+	CALLOC_ARRAY(data, 1);
+	state->listen_data = data;
+	state->listen_error_code = -1;
+	data->shutdown = SHUTDOWN_ERROR;
+
+	fd = inotify_init1(O_NONBLOCK);
+	if (fd < 0)
+		return error_errno("inotify_init1() failed");
+
+	data->fd_inotify = fd;
+
+	hashmap_init(&data->watches, watch_entry_cmp, NULL, 0);
+	hashmap_init(&data->renames, rename_entry_cmp, NULL, 0);
+	hashmap_init(&data->revwatches, revwatches_entry_cmp, NULL, 0);
+
+	if (add_watch(state->path_worktree_watch.buf, data))
+		ret = -1;
+	else if (register_inotify(state->path_worktree_watch.buf, data))
+		ret = -1;
+	else if (state->nr_paths_watching > 1) {
+		if (add_watch(state->path_gitdir_watch.buf, data))
+			ret = -1;
+		else if (register_inotify(state->path_gitdir_watch.buf, data))
+			ret = -1;
+	}
+
+	if (!ret) {
+		state->listen_error_code = 0;
+		data->shutdown = SHUTDOWN_CONTINUE;
+	}
+
+	return ret;
+}
+
+void fsm_listen__dtor(struct fsmonitor_daemon_state *state)
+{
+	struct fsm_listen_data *data;
+	struct hashmap_iter iter;
+	struct watch_entry *w;
+	int fd;
+
+	if (!state || !state->listen_data)
+		return;
+
+	data = state->listen_data;
+	fd = data->fd_inotify;
+
+	hashmap_for_each_entry(&data->watches, &iter, w, ent) {
+		w->cookie = 0; /* ignore any pending renames */
+		remove_watch(w, data);
+	}
+	hashmap_clear(&data->watches);
+
+	hashmap_clear(&data->revwatches); /* remove_watch freed the entries */
+
+	hashmap_clear_and_free(&data->renames, struct rename_entry, ent);
+
+	FREE_AND_NULL(state->listen_data);
+
+	if (fd && (close(fd) < 0))
+		error_errno(_("closing inotify file descriptor failed"));
+}
+
+void fsm_listen__stop_async(struct fsmonitor_daemon_state *state)
+{
+	if (!state->listen_data->shutdown)
+		state->listen_data->shutdown = SHUTDOWN_STOP;
+}
+
+/*
+ * Process a single inotify event and queue for publication.
+ */
+static int process_event(const char *path,
+	const struct inotify_event *event,
+	struct fsmonitor_batch *batch,
+	struct string_list *cookie_list,
+	struct fsmonitor_daemon_state *state)
+{
+	const char *rel;
+	const char *last_sep;
+
+	switch (fsmonitor_classify_path_absolute(state, path)) {
+		case IS_INSIDE_DOT_GIT_WITH_COOKIE_PREFIX:
+		case IS_INSIDE_GITDIR_WITH_COOKIE_PREFIX:
+			/* Use just the filename of the cookie file. */
+			last_sep = find_last_dir_sep(path);
+			string_list_append(cookie_list,
+					last_sep ? last_sep + 1 : path);
+			break;
+		case IS_INSIDE_DOT_GIT:
+		case IS_INSIDE_GITDIR:
+			break;
+		case IS_DOT_GIT:
+		case IS_GITDIR:
+			/*
+			* If .git directory is deleted or renamed away,
+			* we have to quit.
+			*/
+			if (em_dir_deleted(event->mask)) {
+				trace_printf_key(&trace_fsmonitor,
+						"event: gitdir removed");
+				state->listen_data->shutdown = SHUTDOWN_FORCE;
+				goto done;
+			}
+
+			if (em_dir_renamed(event->mask)) {
+				trace_printf_key(&trace_fsmonitor,
+						"event: gitdir renamed");
+				state->listen_data->shutdown = SHUTDOWN_FORCE;
+				goto done;
+			}
+			break;
+		case IS_WORKDIR_PATH:
+			/* normal events in the working directory */
+			if (trace_pass_fl(&trace_fsmonitor))
+				log_mask_set(path, event->mask);
+
+			rel = path + state->path_worktree_watch.len + 1;
+			fsmonitor_batch__add_path(batch, rel);
+
+			if (em_dir_deleted(event->mask))
+				break;
+
+			/* received IN_MOVE_FROM, add tracking for expected IN_MOVE_TO */
+			if (em_rename_dir_from(event->mask))
+				add_dir_rename(event->cookie, path, state->listen_data);
+
+			/* received IN_MOVE_TO, update watch to reflect new path */
+			if (em_rename_dir_to(event->mask))
+				rename_dir(event->cookie, path, state->listen_data);
+
+			if (em_dir_created(event->mask)) {
+				if (add_watch(path, state->listen_data)) {
+					state->listen_data->shutdown = SHUTDOWN_ERROR;
+					goto done;
+				}
+				if (register_inotify(path, state->listen_data)) {
+					state->listen_data->shutdown = SHUTDOWN_ERROR;
+					goto done;
+				}
+			}
+			break;
+		case IS_OUTSIDE_CONE:
+		default:
+			trace_printf_key(&trace_fsmonitor,
+					"ignoring '%s'", path);
+			break;
+	}
+	return 0;
+done:
+	return -1;
+}
+
+/*
+ * Read the inotify event stream and pre-process events before further
+ * processing and eventual publishing.
+ */
+static void handle_events(struct fsmonitor_daemon_state *state)
+{
+	 /* See https://man7.org/linux/man-pages/man7/inotify.7.html */
+	char buf[4096]
+		__attribute__ ((aligned(__alignof__(struct inotify_event))));
+
+	struct hashmap watches = state->listen_data->watches;
+	struct fsmonitor_batch *batch = NULL;
+	struct string_list cookie_list = STRING_LIST_INIT_DUP;
+	struct watch_entry k, *w;
+	struct strbuf path;
+	const struct inotify_event *event;
+	int fd = state->listen_data->fd_inotify;
+	ssize_t len;
+	char *ptr, *p;
+
+	strbuf_init(&path, PATH_MAX);
+
+	for(;;) {
+		len = read(fd, buf, sizeof(buf));
+		if (len == -1 && errno != EAGAIN) {
+			error_errno(_("reading inotify message stream failed"));
+			state->listen_data->shutdown = SHUTDOWN_ERROR;
+			goto done;
+		}
+
+		/* nothing to read */
+		if (len <= 0)
+			goto done;
+
+		/* Loop over all events in the buffer. */
+		for (ptr = buf; ptr < buf + len;
+			 ptr += sizeof(struct inotify_event) + event->len) {
+
+			event = (const struct inotify_event *) ptr;
+
+			if (em_ignore(event->mask))
+				continue;
+
+			/* File system was unmounted or event queue overflowed */
+			if (em_force_shutdown(event->mask)) {
+				if (trace_pass_fl(&trace_fsmonitor))
+					log_mask_set("Forcing shutdown", event->mask);
+				state->listen_data->shutdown = SHUTDOWN_FORCE;
+				goto done;
+			}
+
+			hashmap_entry_init(&k.ent, memhash(&event->wd, sizeof(int)));
+			k.wd = event->wd;
+
+			w = hashmap_get_entry(&watches, &k, ent, NULL);
+			if (!w) /* should never happen */
+				BUG("No watch for '%s'", event->name);
+
+			/* directory watch was removed */
+			if (em_remove_watch(event->mask)) {
+				remove_watch(w, state->listen_data);
+				continue;
+			}
+
+			strbuf_reset(&path);
+			strbuf_add(&path, w->dir, strlen(w->dir));
+			strbuf_addch(&path, '/');
+			strbuf_add(&path, event->name,  strlen(event->name));
+
+			p = fsmonitor__resolve_alias(path.buf, &state->alias);
+			if (!p)
+				p = strbuf_detach(&path, NULL);
+
+			if (!batch)
+				batch = fsmonitor_batch__new();
+
+			if (process_event(p, event, batch, &cookie_list, state)) {
+				free(p);
+				goto done;
+			}
+			free(p);
+		}
+		strbuf_reset(&path);
+		fsmonitor_publish(state, batch, &cookie_list);
+		string_list_clear(&cookie_list, 0);
+		batch = NULL;
+	}
+done:
+	strbuf_release(&path);
+	fsmonitor_batch__free_list(batch);
+	string_list_clear(&cookie_list, 0);
+}
+
+/*
+ * Non-blocking read of the inotify events stream. The inotify fd is polled
+ * every few millisec to help minimize the number of queue overflows.
+ */
+void fsm_listen__loop(struct fsmonitor_daemon_state *state)
+{
+	int poll_num;
+	const int interval = 1000;
+	time_t checked = time(NULL);
+	struct pollfd fds[1];
+	fds[0].fd = state->listen_data->fd_inotify;
+	fds[0].events = POLLIN;
+
+	for(;;) {
+		switch (state->listen_data->shutdown) {
+			case SHUTDOWN_CONTINUE:
+				poll_num = poll(fds, 1, 10);
+				if (poll_num == -1) {
+					if (errno == EINTR)
+						continue;
+					error_errno(_("polling inotify message stream failed"));
+					state->listen_data->shutdown = SHUTDOWN_ERROR;
+					continue;
+				}
+
+				if ((time(NULL) - checked) >= interval) {
+					checked = time(NULL);
+					if (check_stale_dir_renames(&state->listen_data->renames,
+						checked - interval)) {
+						trace_printf_key(&trace_fsmonitor,
+							"Missed IN_MOVED_TO events, forcing shutdown");
+						state->listen_data->shutdown = SHUTDOWN_FORCE;
+						continue;
+					}
+				}
+
+				if (poll_num > 0 && (fds[0].revents & POLLIN))
+					handle_events(state);
+
+				continue;
+			case SHUTDOWN_ERROR:
+				state->listen_error_code = -1;
+				ipc_server_stop_async(state->ipc_server_data);
+				break;
+			case SHUTDOWN_FORCE:
+				state->listen_error_code = 0;
+				ipc_server_stop_async(state->ipc_server_data);
+				break;
+			case SHUTDOWN_STOP:
+			default:
+				state->listen_error_code = 0;
+				break;
+		}
+		return;
+	}
+}
Signature · 1 line
gitgitgadget
#15 lore raw

PATCH 10/12 fsmonitor: enable fsmonitor for Linux

In reply to Eric DeCosta via GitGitGadget

From: Eric DeCosta <[email protected]>

Uodate build to enable fsmonitor for Linux.

---
 config.mak.uname                    |  9 +++++++++
 contrib/buildsystems/CMakeLists.txt | 11 ++++++++++-
 2 files changed, 19 insertions(+), 1 deletion(-)
config.mak.uname +9 -0
index d454cec47c4..04988266835 100644
@@ -69,6 +69,15 @@ ifeq ($(uname_S),Linux)
 		BASIC_CFLAGS += -std=c99
 	endif
 
+	# The builtin FSMonitor on Linux builds upon Simple-IPC.  Both require
+	# Unix domain sockets and PThreads.
+	ifndef NO_PTHREADS
+	ifndef NO_UNIX_SOCKETS
+	FSMONITOR_DAEMON_BACKEND = linux
+	FSMONITOR_OS_SETTINGS = linux
+	FSMONITOR_DAEMON_COMMON = unix
+	endif
+	endif
 endif
 ifeq ($(uname_S),GNU/kFreeBSD)
 	HAVE_ALLOCA_H = YesPlease
contrib/buildsystems/CMakeLists.txt +10 -1
index 0b26a1a36e3..afabfcdefdd 100644
@@ -304,7 +304,16 @@ else()
 endif()
 
 if(SUPPORTS_SIMPLE_IPC)
-	if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
+	if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
+		add_compile_definitions(HAVE_FSMONITOR_DAEMON_BACKEND)
+		list(APPEND compat_SOURCES compat/fsmonitor/fsm-ipc-unix.c)
+		list(APPEND compat_SOURCES compat/fsmonitor/fsm-health-unix.c)
+		list(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-linux.c)
+		list(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-linux.c)
+
+		add_compile_definitions(HAVE_FSMONITOR_OS_SETTINGS)
+		list(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-unix.c)
+	elseif(CMAKE_SYSTEM_NAME STREQUAL "Windows")
 		add_compile_definitions(HAVE_FSMONITOR_DAEMON_BACKEND)
 		list(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-win32.c)
 		list(APPEND compat_SOURCES compat/fsmonitor/fsm-health-win32.c)
Signature · 1 line
gitgitgadget
#16 lore raw

PATCH 11/12 fsmonitor: test updates

In reply to Eric DeCosta via GitGitGadget

From: Eric DeCosta <[email protected]>

t7527-builtin-fsmonitor was leaking fsmonitor--daemon processes in some cases.

Accomodate slight difference in the number of events generated on Linux.

On lower-powered systems, spin a little to give the daemon time to respond to and log filesystem events.

---
 t/t7527-builtin-fsmonitor.sh | 58 +++++++++++++++++++++++++++---------
 1 file changed, 44 insertions(+), 14 deletions(-)
t/t7527-builtin-fsmonitor.sh +44 -14
index 56c0dfffea6..19a243588e2 100755
@@ -13,7 +13,7 @@ fi
 stop_daemon_delete_repo () {
 	r=$1 &&
 	test_might_fail git -C $r fsmonitor--daemon stop &&
-	rm -rf $1
+	rm -rf $r
 }
 
 start_daemon () {
@@ -72,6 +72,32 @@ start_daemon () {
 	)
 }
 
+IMPLICIT_TIMEOUT=5
+
+wait_for_update () {
+	func=$1 &&
+	file=$2 &&
+	sz=$(wc -c < "$file") &&
+	last=0 &&
+	$func &&
+	k=0 &&
+	while test "$k" -lt $IMPLICIT_TIMEOUT
+	do
+		nsz=$(wc -c < "$file")
+		if test "$nsz" -gt "$sz"
+		then
+			if test "$last" -eq "$nsz"
+			then
+				return 0
+			fi
+			last=$nsz
+		fi
+		sleep 1
+		k=$(( $k + 1 ))
+	done &&
+	return 0
+}
+
 # Is a Trace2 data event present with the given catetory and key?
 # We do not care what the value is.
 #
@@ -137,7 +163,6 @@ test_expect_success 'implicit daemon start' '
 # machines (where it might take a moment to wake and reschedule the
 # daemon process) to avoid false alarms during test runs.)
 #
-IMPLICIT_TIMEOUT=5
 
 verify_implicit_shutdown () {
 	r=$1 &&
@@ -373,6 +398,10 @@ create_files () {
 	echo 3 >dir2/new
 }
 
+rename_directory () {
+	mv dirtorename dirrenamed
+}
+
 rename_files () {
 	mv rename renamed &&
 	mv dir1/rename dir1/renamed &&
@@ -427,7 +456,7 @@ test_expect_success 'edit some files' '
 
 	start_daemon --tf "$PWD/.git/trace" &&
 
-	edit_files &&
+	wait_for_update edit_files "$PWD/.git/trace" &&
 
 	test-tool fsmonitor-client query --token 0 &&
 
@@ -442,7 +471,7 @@ test_expect_success 'create some files' '
 
 	start_daemon --tf "$PWD/.git/trace" &&
 
-	create_files &&
+	wait_for_update create_files "$PWD/.git/trace" &&
 
 	test-tool fsmonitor-client query --token 0 &&
 
@@ -456,7 +485,7 @@ test_expect_success 'delete some files' '
 
 	start_daemon --tf "$PWD/.git/trace" &&
 
-	delete_files &&
+	wait_for_update delete_files "$PWD/.git/trace" &&
 
 	test-tool fsmonitor-client query --token 0 &&
 
@@ -470,7 +499,7 @@ test_expect_success 'rename some files' '
 
 	start_daemon --tf "$PWD/.git/trace" &&
 
-	rename_files &&
+	wait_for_update rename_files "$PWD/.git/trace" &&
 
 	test-tool fsmonitor-client query --token 0 &&
 
@@ -487,7 +516,7 @@ test_expect_success 'rename directory' '
 
 	start_daemon --tf "$PWD/.git/trace" &&
 
-	mv dirtorename dirrenamed &&
+	wait_for_update rename_directory "$PWD/.git/trace" &&
 
 	test-tool fsmonitor-client query --token 0 &&
 
@@ -500,7 +529,7 @@ test_expect_success 'file changes to directory' '
 
 	start_daemon --tf "$PWD/.git/trace" &&
 
-	file_to_directory &&
+	wait_for_update file_to_directory "$PWD/.git/trace" &&
 
 	test-tool fsmonitor-client query --token 0 &&
 
@@ -513,7 +542,7 @@ test_expect_success 'directory changes to a file' '
 
 	start_daemon --tf "$PWD/.git/trace" &&
 
-	directory_to_file &&
+	wait_for_update directory_to_file "$PWD/.git/trace" &&
 
 	test-tool fsmonitor-client query --token 0 &&
 
@@ -561,7 +590,7 @@ test_expect_success 'flush cached data' '
 	test-tool -C test_flush fsmonitor-client query --token "builtin:test_00000002:0" >actual_2 &&
 	nul_to_q <actual_2 >actual_q2 &&
 
-	grep "^builtin:test_00000002:0Q$" actual_q2 &&
+	grep "^builtin:test_00000002:[0-1]Q$" actual_q2 &&
 
 	>test_flush/file_3 &&
 
@@ -732,7 +761,8 @@ u_values="$u1 $u2"
 for u in $u_values
 do
 	test_expect_success "unicode in repo root path: $u" '
-		test_when_finished "stop_daemon_delete_repo $u" &&
+		test_when_finished \
+		"stop_daemon_delete_repo `echo "$u" | sed 's:x:\\\\\\\\\\\\\\x:g'`" &&
 
 		git init "$u" &&
 		echo 1 >"$u"/file1 &&
@@ -814,8 +844,7 @@ my_match_and_clean () {
 }
 
 test_expect_success 'submodule always visited' '
-	test_when_finished "git -C super fsmonitor--daemon stop; \
-			    rm -rf super; \
+	test_when_finished "rm -rf super; \
 			    rm -rf sub" &&
 
 	create_super super &&
@@ -883,7 +912,8 @@ have_t2_error_event () {
 }
 
 test_expect_success "stray submodule super-prefix warning" '
-	test_when_finished "rm -rf super; \
+	test_when_finished "git -C super/dir_1/dir_2/sub fsmonitor--daemon stop; \
+			    rm -rf super; \
 			    rm -rf sub;   \
 			    rm super-sub.trace" &&
 
Signature · 1 line
gitgitgadget
#18 lore raw

PATCH 12/12 fsmonitor: update doc for Linux

In reply to Eric DeCosta via GitGitGadget

From: Eric DeCosta <[email protected]>

Update the documentation for Linux.

---
 Documentation/config/fsmonitor--daemon.txt |  4 ++--
 Documentation/git-fsmonitor--daemon.txt    | 24 ++++++++++++++--------
 2 files changed, 18 insertions(+), 10 deletions(-)
Documentation/config/fsmonitor--daemon.txt +2 -2
index c225c6c9e74..2cafb040d96 100644
@@ -4,8 +4,8 @@ fsmonitor.allowRemote::
     behavior.  Only respected when `core.fsmonitor` is set to `true`.
 
 fsmonitor.socketDir::
-    This Mac OS-specific option, if set, specifies the directory in
+    Mac OS and Linux-specific option. If set, specifies the directory in
     which to create the Unix domain socket used for communication
     between the fsmonitor daemon and various Git commands. The directory must
-    reside on a native Mac OS filesystem.  Only respected when `core.fsmonitor`
+    reside on a native filesystem.  Only respected when `core.fsmonitor`
     is set to `true`.
Documentation/git-fsmonitor--daemon.txt +16 -8
index 8238eadb0e1..c2b08229c74 100644
@@ -76,23 +76,31 @@ repositories; this may be overridden by setting `fsmonitor.allowRemote` to
 correctly with all network-mounted repositories and such use is considered
 experimental.
 
-On Mac OS, the inter-process communication (IPC) between various Git
+On Linux and Mac OS, the inter-process communication (IPC) between various Git
 commands and the fsmonitor daemon is done via a Unix domain socket (UDS) -- a
-special type of file -- which is supported by native Mac OS filesystems,
-but not on network-mounted filesystems, NTFS, or FAT32.  Other filesystems
-may or may not have the needed support; the fsmonitor daemon is not guaranteed
-to work with these filesystems and such use is considered experimental.
+special type of file -- which is supported by many native Linux and Mac OS
+filesystems, but not on network-mounted filesystems, NTFS, or FAT32.  Other
+filesystems may or may not have the needed support; the fsmonitor daemon is not
+guaranteed to work with these filesystems and such use is considered
+experimental.
 
 By default, the socket is created in the `.git` directory, however, if the
 `.git` directory is on a network-mounted filesystem, it will be instead be
 created at `$HOME/.git-fsmonitor-*` unless `$HOME` itself is on a
 network-mounted filesystem in which case you must set the configuration
-variable `fsmonitor.socketDir` to the path of a directory on a Mac OS native
+variable `fsmonitor.socketDir` to the path of a directory on a native
 filesystem in which to create the socket file.
 
 If none of the above directories (`.git`, `$HOME`, or `fsmonitor.socketDir`)
-is on a native Mac OS file filesystem the fsmonitor daemon will report an
-error that will cause the daemon and the currently running command to exit.
+is on a native Linux or Mac OS filesystem the fsmonitor daemon will report
+an error that will cause the daemon to exit and the currently running command
+to issue a warning.
+
+On Linux, the fsmonitor daemon registers a watch for each directory in the
+repository.  The default per-user limit for the number of watches on most Linux
+systems is 8192.  This may not be sufficient for large repositories or if
+multiple instances of the fsmonitor daemon are running.
+See https://watchexec.github.io/docs/inotify-limits.html[Linux inotify limits] for more information.
 
 CONFIGURATION
 -------------
Signature · 1 line
gitgitgadget
#9 lore raw

Re: [PATCH 07/12] fsmonitor: prepare to share code between Mac OS and Linux

In reply to Eric DeCosta via GitGitGadget
Quoted 10 lines · Eric DeCosta via GitGitGadget
Makefile +0 -2
index feb675a6959..31dd6ab2734 100644
@@ -2038,13 +2038,13 @@ endif
 ifdef FSMONITOR_DAEMON_BACKEND
 	COMPAT_CFLAGS += -DHAVE_FSMONITOR_DAEMON_BACKEND
 	COMPAT_OBJS += compat/fsmonitor/fsm-listen-$(FSMONITOR_DAEMON_BACKEND).o
-	COMPAT_OBJS += compat/fsmonitor/fsm-health-$(FSMONITOR_DAEMON_BACKEND).o
-	COMPAT_OBJS += compat/fsmonitor/fsm-ipc-$(FSMONITOR_DAEMON_BACKEND).o

Makefile endif · line 2049

Quoted 10 lines
Makefile +3 -1
+	COMPAT_OBJS += compat/fsmonitor/fsm-health-$(FSMONITOR_DAEMON_COMMON).o
+	COMPAT_OBJS += compat/fsmonitor/fsm-ipc-$(FSMONITOR_DAEMON_COMMON).o
 endif
 
 ifdef FSMONITOR_OS_SETTINGS
 	COMPAT_CFLAGS += -DHAVE_FSMONITOR_OS_SETTINGS
-	COMPAT_OBJS += compat/fsmonitor/fsm-settings-$(FSMONITOR_OS_SETTINGS).o
+	COMPAT_OBJS += compat/fsmonitor/fsm-settings-$(FSMONITOR_DAEMON_COMMON).o
 	COMPAT_OBJS += compat/fsmonitor/fsm-path-utils-$(FSMONITOR_OS_SETTINGS).o
 endif

These two look sloppier than the existing "if we have DAEMON_BACKEND defined, then add $(DAEMON_BACKEND).o to the set of objects used". $(DAEMON_COMMON).o should be used after the same kind of check, i.e.

+ifdef FSMONITOR_DAEMON_COMMON
+ COMPAT_OBJS += compat/fsmonitor/fsm-health-$(FSMONITOR_DAEMON_COMMON).o
+ COMPAT_OBJS += compat/fsmonitor/fsm-ipc-$(FSMONITOR_DAEMON_COMMON).o
+ COMPAT_OBJS += compat/fsmonitor/fsm-settings-$(FSMONITOR_DAEMON_COMMON).o
+endif

as a separate if/endif block, without touching the above two, perhaps?

Quoted 13 lines
compat/fsmonitor/fsm-ipc-unix.c renamed from compat/fsmonitor/fsm-ipc-darwin.c +1 -1
@@ -10,7 +10,7 @@ static GIT_PATH_FUNC(fsmonitor_ipc__get_default_path, "fsmonitor--daemon.ipc")
 const char *fsmonitor_ipc__get_path(struct repository *r)
 {
 	static const char *ipc_path = NULL;
-	SHA_CTX sha1ctx;
+	git_SHA_CTX sha1ctx;
 	char *sock_dir = NULL;
 	struct strbuf ipc_file = STRBUF_INIT;
 	unsigned char hash[SHA_DIGEST_LENGTH];
@@ -28,9 +28,9 @@ const char *fsmonitor_ipc__get_path(struct repository *r)
 		return ipc_path;

compat/fsmonitor/fsm-ipc-unix.c const char *fsmonitor_ipc__get_path(struct repository *r) · line 35

Quoted 10 lines
compat/fsmonitor/fsm-ipc-unix.c renamed from compat/fsmonitor/fsm-ipc-darwin.c +3 -3
 	}
 
-	SHA1_Init(&sha1ctx);
-	SHA1_Update(&sha1ctx, r->worktree, strlen(r->worktree));
-	SHA1_Final(hash, &sha1ctx);
+	git_SHA1_Init(&sha1ctx);
+	git_SHA1_Update(&sha1ctx, r->worktree, strlen(r->worktree));
+	git_SHA1_Final(hash, &sha1ctx);
 
 	repo_config_get_string(r, "fsmonitor.socketdir", &sock_dir);

Interesting. The result of course is good, but I wonder how we have been happy to queue the original code that lack git_ prefix in the first place X-<.

config.mak.uname ifeq ($(uname_S),Linux) · line 74

Quoted 12 lines
config.mak.uname +1 -0
index d63629fe807..d454cec47c4 100644
@@ -68,6 +68,7 @@ ifeq ($(uname_S),Linux)
 	ifneq ($(findstring .el7.,$(uname_R)),)
 		BASIC_CFLAGS += -std=c99
 	endif
+
 endif
 ifeq ($(uname_S),GNU/kFreeBSD)
 	HAVE_ALLOCA_H = YesPlease

That's a new blank line in an unexpected place. Did you mean to add it after the outer endif?

#20 lore raw
In reply to Eric DeCosta via GitGitGadget
Quoted 5 lines · Eric DeCosta via GitGitGadget

Goal is to deliver fsmonitor for Linux that is on par with fsmonitor for Windows and Mac OS.

This patch set builds upon previous work for done for Windows and Mac OS (first 6 patches) ...

For those who are watching from sidelines...

The Windows part is already in Git 2.38; the changes needed for macOS are already in 'next' and the first 6 patches in this 12-patch series are identical to them. The patches 7-12 are new.

Quoted 5 lines

to implement a fsmonitor back-end for Linux based on the
Linux inotify API. inotify differs significantly from the equivalent Windows and Mac OS APIs in that a watch must be registered for every directory of interest (rather than a singular watch at the root of the directory tree) and special care must be taken to handle directory renames correctly.

#21 lore raw
Quoted 4 lines · Junio C Hamano
Quoted 3 lines · Eric DeCosta via GitGitGadget

Goal is to deliver fsmonitor for Linux that is on par with fsmonitor for Windows and Mac OS.

Quoted 8 lines
Quoted 2 lines

This patch set builds upon previous work for done for Windows and Mac OS (first 6 patches) ...

For those who are watching from sidelines...

The Windows part is already in Git 2.38; the changes needed for macOS are already in 'next' and the first 6 patches in this 12-patch series are identical to them. The patches 7-12 are new.

Thanks for clarifying. I found it confusing that there were a number of patches in this series which I had already seen despite the cover letter's claim that this series builds upon "previous work". Thus, it wasn't clear whether this series was a reroll (refining some already-seen patches) with additional patches for Linux, or if it was purely new work with the original patches included by accident[1].

[1]: In the few times I've used GitGitGadget, I've had to jump through hoops to make it send just the "new" patches when I've built a series atop some other series only in 'next' or 'seen', so I can understand the inclusion of the first six patches being accidental. (Regarding the hoop-jumping, it may be that I just don't understand how to "work" GitGitGadget or GitHub pull-requests.)

#12 lore raw

Re: [PATCH 08/12] fsmonitor: determine if filesystem is local or remote

In reply to Eric DeCosta via GitGitGadget
Quoted 42 lines · Eric DeCosta via GitGitGadget

From: Eric DeCosta <[email protected]>

Compare the given path to the mounted filesystems. Find the mount that is the longest prefix of the path (if any) and determine if that mount is on a local or remote filesystem.

---
 compat/fsmonitor/fsm-path-utils-linux.c | 163 ++++++++++++++++++++++++
 1 file changed, 163 insertions(+)
 create mode 100644 compat/fsmonitor/fsm-path-utils-linux.c
compat/fsmonitor/fsm-path-utils-linux.c new file +24 -0
new file mode 100644
index 00000000000..369692a788f
@@ -0,0 +1,163 @@
+#include "fsmonitor.h"
+#include "fsmonitor-path-utils.h"
+#include <errno.h>
+#include <mntent.h>
+#include <sys/mount.h>
+#include <sys/statvfs.h>
+
+/*
+ * https://github.com/coreutils/gnulib/blob/master/lib/mountlist.c
+ */
+#ifndef ME_REMOTE
+/* A file system is "remote" if its Fs_name contains a ':'
+   or if (it is of type (smbfs or cifs) and its Fs_name starts with '//')
+   or if it is of any other of the listed types
+   or Fs_name is equal to "-hosts" (used by autofs to mount remote fs).
+   "VM" file systems like prl_fs or vboxsf are not considered remote here. */
+# define ME_REMOTE(Fs_name, Fs_type)            \
+	(strchr (Fs_name, ':') != NULL              \
+	 || ((Fs_name)[0] == '/'                    \
+		 && (Fs_name)[1] == '/'                 \
+		 && (strcmp (Fs_type, "smbfs") == 0     \
+			 || strcmp (Fs_type, "smb3") == 0   \
+			 || strcmp (Fs_type, "cifs") == 0)) \
+	 || strcmp (Fs_type, "acfs") == 0           \

compat/fsmonitor/fsm-path-utils-linux.c line 34

Quoted 10 lines
compat/fsmonitor/fsm-path-utils-linux.c new file +10 -0
+	 || strcmp (Fs_type, "afs") == 0            \
+	 || strcmp (Fs_type, "coda") == 0           \
+	 || strcmp (Fs_type, "auristorfs") == 0     \
+	 || strcmp (Fs_type, "fhgfs") == 0          \
+	 || strcmp (Fs_type, "gpfs") == 0           \
+	 || strcmp (Fs_type, "ibrix") == 0          \
+	 || strcmp (Fs_type, "ocfs2") == 0          \
+	 || strcmp (Fs_type, "vxfs") == 0           \
+	 || strcmp ("-hosts", Fs_name) == 0)
+#endif

So, this is just copy/pasted GPLv3 code into our GPLv2-only codebase?:
https://github.com/coreutils/gnulib/blob/cd1fdabe8b66c102124b6a5b0c97dded20246b46/lib/mountlist.c#L230-L247

Quoted 4 lines
compat/fsmonitor/fsm-path-utils-linux.c new file +4 -0
+static struct mntent *find_mount(const char *path, const struct statvfs *fs)
+{
+	const char *const mounts = "/proc/mounts";
+	const char *rp = real_pathdup(path, 1);

compat/fsmonitor/fsm-path-utils-linux.c line 49

Quoted 10 lines
compat/fsmonitor/fsm-path-utils-linux.c new file +10 -0
+	struct mntent *ment = NULL;
+	struct mntent *found = NULL;
+	struct statvfs mntfs;
+	FILE *fp;
+	int dlen, plen, flen = 0;
+
+	fp = setmntent(mounts, "r");
+	if (!fp) {
+		error_errno(_("setmntent('%s') failed"), mounts);
+		return NULL;

This code would probably be nicer if you returned int, and passed a pointer to a struct to populate as a paremeter. Then you could just "return error..." for this and the below cases.

Quoted 30 lines
compat/fsmonitor/fsm-path-utils-linux.c new file +30 -0
+	}
+
+	plen = strlen(rp);
+
+	/* read all the mount information and compare to path */
+	while ((ment = getmntent(fp)) != NULL) {
+		if (statvfs(ment->mnt_dir, &mntfs)) {
+			switch (errno) {
+			case EPERM:
+			case ESRCH:
+			case EACCES:
+				continue;
+			default:
+				error_errno(_("statvfs('%s') failed"), ment->mnt_dir);
+				endmntent(fp);is 
+				return NULL;
+			}
+		}
+
+		/* is mount on the same filesystem and is a prefix of the path */
+		if ((fs->f_fsid == mntfs.f_fsid) &&
+			!strncmp(ment->mnt_dir, rp, strlen(ment->mnt_dir))) {
+			dlen = strlen(ment->mnt_dir);
+			if (dlen > plen)
+				continue;
+			/*
+			 * root is always a potential match; otherwise look for
+			 * directory prefix
+			 */
+			if ((dlen == 1 && ment->mnt_dir[0] == '/') ||

compat/fsmonitor/fsm-path-utils-linux.c line 89

Quoted 10 lines
compat/fsmonitor/fsm-path-utils-linux.c new file +10 -0
+				(dlen > flen && (!rp[dlen] || rp[dlen] == '/'))) {
+				flen = dlen;
+				/*
+				 * https://man7.org/linux/man-pages/man3/getmntent.3.html
+				 *
+				 * The pointer points to a static area of memory which is
+				 * overwritten by subsequent calls to getmntent().
+				 */
+				if (!found)
+					CALLOC_ARRAY(found, 1);

It seems we never populate >1 of these, so don't you just want xmalloc(). Or actually...

compat/fsmonitor/fsm-path-utils-linux.c line 95

Quoted 6 lines
compat/fsmonitor/fsm-path-utils-linux.c new file +6 -0
+				free(found->mnt_dir);
+				free(found->mnt_type);
+				free(found->mnt_fsname);
+				found->mnt_dir = xmemdupz(ment->mnt_dir, strlen(ment->mnt_dir));
+				found->mnt_type = xmemdupz(ment->mnt_type, strlen(ment->mnt_type));
+				found->mnt_fsname = xmemdupz(ment->mnt_fsname, strlen(ment->mnt_fsname));

Don't mix mem*() and str*(). In this case we need the string to be '\0' delimited, so use xstrndup().

And once we do that, we might wonder why we're explicitly finding the '\0', just to pass it to the xstrn*() function, when we can just do:

	found->mnt_dir = xstrdup(ment->mnt_dir);
	...

Which would AFAICT be equivalent to what you're doing here.

compat/fsmonitor/fsm-path-utils-linux.c line 106

Quoted 11 lines
compat/fsmonitor/fsm-path-utils-linux.c new file +11 -0
+			}
+		}
+	}
+	endmntent(fp);
+
+	return found;
+}
+
+int fsmonitor__get_fs_info(const char *path, struct fs_info *fs_info)
+{
+	struct mntent *ment;

...make this (or above) a "struct mntent ment", then pass &ment down, so we can fill that struct? Dunno...

Quoted 7 lines
compat/fsmonitor/fsm-path-utils-linux.c new file +7 -0
+	struct statvfs fs;
+
+	if (statvfs(path, &fs))
+		return error_errno(_("statvfs('%s') failed"), path);
+
+	ment = find_mount(path, &fs);
+	if (!ment)

compat/fsmonitor/fsm-path-utils-linux.c line 123

Quoted 10 lines
compat/fsmonitor/fsm-path-utils-linux.c new file +10 -0
+		return -1;
+
+	trace_printf_key(&trace_fsmonitor,
+			 "statvfs('%s') [flags 0x%08lx] '%s' '%s'",
+			 path, fs.f_flag, ment->mnt_type, ment->mnt_fsname);
+
+	if (ME_REMOTE(ment->mnt_fsname, ment->mnt_type))
+		fs_info->is_remote = 1;
+	else
+		fs_info->is_remote = 0;

Shorter:

	fs_info->is_remote = ME_REMOTE(ment->mnt_fsname, ment->mnt_type);
#10 lore raw

Re: [PATCH 07/12] fsmonitor: prepare to share code between Mac OS and Linux

In reply to Eric DeCosta via GitGitGadget
Quoted 9 lines · Eric DeCosta via GitGitGadget

From: Eric DeCosta <[email protected]>

Linux and Mac OS can share some of the code originally developed for Mac OS. Rename the shared code from compat/fsmonitor/fsm-*-dawrin.c to compat/fsmonitor/fsm-*-unix.c

Update the build to enable sharing of the fsm-*-unix.c files.

Minor update to compat/fsmonitor/fsm-ipc-unix.c to make it cross-platform.

I fear that it may be too early to claim that the platform-specific code can truly be shared (at least without some amount of future ifdef'ing). Yes, there are some commonalities, such as our use of Unix domain sockets on Mac and *nix, but there may be other things that are not.

So maybe do the rename for the IPC code, but not for the health, for example. Or, keep the platform-named files and add new -unix-common.c files that can be called from the other (or vice versa).

Quoted 22 lines
---
  Makefile                                                  | 6 +++---
  .../fsmonitor/{fsm-health-darwin.c => fsm-health-unix.c}  | 0
  compat/fsmonitor/{fsm-ipc-darwin.c => fsm-ipc-unix.c}     | 8 ++++----
  .../{fsm-settings-darwin.c => fsm-settings-unix.c}        | 0
  config.mak.uname                                          | 4 ++++
  contrib/buildsystems/CMakeLists.txt                       | 6 +++---
  6 files changed, 14 insertions(+), 10 deletions(-)

rename compat/fsmonitor/{fsm-health-darwin.c => fsm-health-unix.c} (100%) rename compat/fsmonitor/{fsm-ipc-darwin.c => fsm-ipc-unix.c} (89%) rename compat/fsmonitor/{fsm-settings-darwin.c => fsm-settings-unix.c} (100%)

Makefile +0 -0
index feb675a6959..31dd6ab2734 100644
@@ -2038,13 +2038,13 @@ endif
  ifdef FSMONITOR_DAEMON_BACKEND
  	COMPAT_CFLAGS += -DHAVE_FSMONITOR_DAEMON_BACKEND
  	COMPAT_OBJS += compat/fsmonitor/fsm-listen-$(FSMONITOR_DAEMON_BACKEND).o

Makefile endif · line 2047

Quoted 10 lines
Makefile +3 -3
-	COMPAT_OBJS += compat/fsmonitor/fsm-health-$(FSMONITOR_DAEMON_BACKEND).o
-	COMPAT_OBJS += compat/fsmonitor/fsm-ipc-$(FSMONITOR_DAEMON_BACKEND).o
+	COMPAT_OBJS += compat/fsmonitor/fsm-health-$(FSMONITOR_DAEMON_COMMON).o
+	COMPAT_OBJS += compat/fsmonitor/fsm-ipc-$(FSMONITOR_DAEMON_COMMON).o
  endif
 
  ifdef FSMONITOR_OS_SETTINGS
  	COMPAT_CFLAGS += -DHAVE_FSMONITOR_OS_SETTINGS
-	COMPAT_OBJS += compat/fsmonitor/fsm-settings-$(FSMONITOR_OS_SETTINGS).o
+	COMPAT_OBJS += compat/fsmonitor/fsm-settings-$(FSMONITOR_DAEMON_COMMON).o

This is wrong. you're mixing the usage of the FSMONITOR_OS_SETTINGS definition and the usage of your new _COMMON definition.

And I agree with Junio's comment about grouping the _COMMON ones in their own block and leaving the original COMPAT_OBJS lines as they were.

But really, I think the use of that "common" definition is going to cause us problems later on. I mean, you're using it to smash together the ipc, health, and settings code -- because they cluster around the unix sockets and how on unix to tell if something is remote. That grouping may break if a platform needs something else, such as how to behave when running under inetd vs launchctl vs <whatever>, for example.

Quoted 15 lines
Makefile +0 -0
  	COMPAT_OBJS += compat/fsmonitor/fsm-path-utils-$(FSMONITOR_OS_SETTINGS).o
  endif
 
compat/fsmonitor/fsm-health-unix.c renamed from compat/fsmonitor/fsm-health-darwin.c +0 -0
similarity index 100%
rename from compat/fsmonitor/fsm-health-darwin.c
rename to compat/fsmonitor/fsm-health-unix.c
compat/fsmonitor/fsm-ipc-unix.c renamed from compat/fsmonitor/fsm-ipc-darwin.c +0 -0
similarity index 89%
rename from compat/fsmonitor/fsm-ipc-darwin.c
rename to compat/fsmonitor/fsm-ipc-unix.c
index ce843d63348..3ba3b9e17ed 100644

compat/fsmonitor/fsm-ipc-unix.c static GIT_PATH_FUNC(fsmonitor_ipc__get_default_path, "fsmonitor--daemon.ipc") · line 13

Quoted 5 lines
compat/fsmonitor/fsm-ipc-unix.c renamed from compat/fsmonitor/fsm-ipc-darwin.c +1 -1
@@ -10,7 +10,7 @@ static GIT_PATH_FUNC(fsmonitor_ipc__get_default_path, "fsmonitor--daemon.ipc")
  const char *fsmonitor_ipc__get_path(struct repository *r)
  {
  	static const char *ipc_path = NULL;
-	SHA_CTX sha1ctx;
+	git_SHA_CTX sha1ctx;

Could we put these in a separate commit. They're not related to the file renaming focus of this commit.

Quoted 78 lines
compat/fsmonitor/fsm-ipc-unix.c renamed from compat/fsmonitor/fsm-ipc-darwin.c +3 -3
  	char *sock_dir = NULL;
  	struct strbuf ipc_file = STRBUF_INIT;
  	unsigned char hash[SHA_DIGEST_LENGTH];
@@ -28,9 +28,9 @@ const char *fsmonitor_ipc__get_path(struct repository *r)
  		return ipc_path;
  	}
 
-	SHA1_Init(&sha1ctx);
-	SHA1_Update(&sha1ctx, r->worktree, strlen(r->worktree));
-	SHA1_Final(hash, &sha1ctx);
+	git_SHA1_Init(&sha1ctx);
+	git_SHA1_Update(&sha1ctx, r->worktree, strlen(r->worktree));
+	git_SHA1_Final(hash, &sha1ctx);
 
  	repo_config_get_string(r, "fsmonitor.socketdir", &sock_dir);
 
compat/fsmonitor/fsm-settings-unix.c renamed from compat/fsmonitor/fsm-settings-darwin.c +0 -0
similarity index 100%
rename from compat/fsmonitor/fsm-settings-darwin.c
rename to compat/fsmonitor/fsm-settings-unix.c
config.mak.uname +4 -0
index d63629fe807..d454cec47c4 100644
@@ -68,6 +68,7 @@ ifeq ($(uname_S),Linux)
  	ifneq ($(findstring .el7.,$(uname_R)),)
  		BASIC_CFLAGS += -std=c99
  	endif
+
  endif
  ifeq ($(uname_S),GNU/kFreeBSD)
  	HAVE_ALLOCA_H = YesPlease
@@ -165,6 +166,7 @@ ifeq ($(uname_S),Darwin)
  	ifndef NO_UNIX_SOCKETS
  	FSMONITOR_DAEMON_BACKEND = darwin
  	FSMONITOR_OS_SETTINGS = darwin
+	FSMONITOR_DAEMON_COMMON = unix
  	endif
  	endif
 
@@ -453,6 +455,7 @@ ifeq ($(uname_S),Windows)
  	# support it.
  	FSMONITOR_DAEMON_BACKEND = win32
  	FSMONITOR_OS_SETTINGS = win32
+	FSMONITOR_DAEMON_COMMON = win32
 
  	NO_SVN_TESTS = YesPlease
  	RUNTIME_PREFIX = YesPlease
@@ -645,6 +648,7 @@ ifeq ($(uname_S),MINGW)
  	# support it.
  	FSMONITOR_DAEMON_BACKEND = win32
  	FSMONITOR_OS_SETTINGS = win32
+	FSMONITOR_DAEMON_COMMON = win32
 
  	RUNTIME_PREFIX = YesPlease
  	HAVE_WPGMPTR = YesWeDo
contrib/buildsystems/CMakeLists.txt +3 -3
index 787738e6fa3..0b26a1a36e3 100644
@@ -315,13 +315,13 @@ if(SUPPORTS_SIMPLE_IPC)
  		list(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-win32.c)
  	elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
  		add_compile_definitions(HAVE_FSMONITOR_DAEMON_BACKEND)
+		list(APPEND compat_SOURCES compat/fsmonitor/fsm-ipc-unix.c)
+		list(APPEND compat_SOURCES compat/fsmonitor/fsm-health-unix.c)
  		list(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-darwin.c)
-		list(APPEND compat_SOURCES compat/fsmonitor/fsm-health-darwin.c)
-		list(APPEND compat_SOURCES compat/fsmonitor/fsm-ipc-darwin.c)
  		list(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-darwin.c)
 
  		add_compile_definitions(HAVE_FSMONITOR_OS_SETTINGS)
-		list(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-darwin.c)
+		list(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-unix.c)
  	endif()
  endif()
 
#22 lore raw
In reply to Eric DeCosta via GitGitGadget
Quoted 9 lines · Eric DeCosta via GitGitGadget

Goal is to deliver fsmonitor for Linux that is on par with fsmonitor for Windows and Mac OS.

This patch set builds upon previous work for done for Windows and Mac OS (first 6 patches) to implement a fsmonitor back-end for Linux based on the Linux inotify API. inotify differs significantly from the equivalent Windows and Mac OS APIs in that a watch must be registered for every directory of interest (rather than a singular watch at the root of the directory tree) and special care must be taken to handle directory renames correctly.

I am seeing occasional breakages of t7527.16 that does not seem to reproduce reliably.

#13 lore raw

RE: [PATCH 08/12] fsmonitor: determine if filesystem is local or remote

In reply to Ævar Arnfjörð Bjarmason
Quoted 62 lines

-----Original Message-----
From: Ævar Arnfjörð Bjarmason <[email protected]>
Sent: Monday, October 10, 2022 6:04 AM
To: Eric DeCosta via GitGitGadget <[email protected]>
Cc: [email protected]; Eric DeCosta <[email protected]>
Subject: Re: [PATCH 08/12] fsmonitor: determine if filesystem is local or remote

Quoted 9 lines · Eric DeCosta via GitGitGadget

From: Eric DeCosta <[email protected]>

Compare the given path to the mounted filesystems. Find the mount that is the longest prefix of the path (if any) and determine if that mount is on a local or remote filesystem.

---
compat/fsmonitor/fsm-path-utils-linux.c | 163

++++++++++++++++++++++++

Quoted 10 lines
1 file changed, 163 insertions(+)

create mode 100644 compat/fsmonitor/fsm-path-utils-linux.c

compat/fsmonitor/fsm-path-utils-linux.c new file +0 -0
new file mode 100644
index 00000000000..369692a788f

compat/fsmonitor/fsm-path-utils-linux.c line 10

Quoted 10 lines
compat/fsmonitor/fsm-path-utils-linux.c new file +10 -0
@@ -0,0 +1,163 @@
+#include "fsmonitor.h"
+#include "fsmonitor-path-utils.h"
+#include <errno.h>
+#include <mntent.h>
+#include <sys/mount.h>
+#include <sys/statvfs.h>
+
+/*
+ * https://github.com/coreutils/gnulib/blob/master/lib/mountlist.c
+<https://protect-

us.mimecast.com/s/wKJsC9rj7lc3WKmYUOq9E9?domain=gith

Quoted 18 lines
compat/fsmonitor/fsm-path-utils-linux.c new file +18 -0
+ub.com>
+ */
+#ifndef ME_REMOTE
+/* A file system is "remote" if its Fs_name contains a ':'
+ or if (it is of type (smbfs or cifs) and its Fs_name starts with
+'//')  or if it is of any other of the listed types  or Fs_name is
+equal to "-hosts" (used by autofs to mount remote fs).
+ "VM" file systems like prl_fs or vboxsf are not considered remote
+here. */ # define ME_REMOTE(Fs_name, Fs_type) \  (strchr (Fs_name,
+':') != NULL \
+ || ((Fs_name)[0] == '/' \
+ && (Fs_name)[1] == '/' \
+ && (strcmp (Fs_type, "smbfs") == 0 \
+ || strcmp (Fs_type, "smb3") == 0 \
+ || strcmp (Fs_type, "cifs") == 0)) \ strcmp (Fs_type, "acfs") == 0 \
+ || strcmp (Fs_type, "afs") == 0 \ strcmp (Fs_type, "coda") == 0 \
+ || strcmp (Fs_type, "auristorfs") == 0 \ strcmp (Fs_type, "fhgfs")
+ || == 0 \ strcmp (Fs_type, "gpfs") == 0 \ strcmp (Fs_type, "ibrix")
Quoted 9 lines

compat/fsmonitor/fsm-path-utils-linux.c line 31

Quoted 3 lines
compat/fsmonitor/fsm-path-utils-linux.c new file +3 -0
+ || == 0 \ strcmp (Fs_type, "ocfs2") == 0 \ strcmp (Fs_type, "vxfs")
+ || == 0 \ strcmp ("-hosts", Fs_name) == 0)
+#endif

So, this is just copy/pasted GPLv3 code into our GPLv2-only codebase?:
https://github.com/coreutils/gnulib/blob/cd1fdabe8b66c102124b6a5b0c97d
ded20246b46/lib/mountlist.c#L230-L247 <https://protect-
us.mimecast.com/s/zxPbC0RMy1hoXW2rCOSXJD?domain=github.com>

Yes. I was hoping for some guidance as to what to do with ME_REMOTE.

I also found it, verbatim, here in midnight commander:
https://github.com/MidnightCommander/mc/blob/e48cd98ac13a9b4366bd88287f632413766b967f/src/filemanager/mountlist.c#L258-L281

But that's just another GPLv3 code base.

Quoted 107 lines

compat/fsmonitor/fsm-path-utils-linux.c line 40

Quoted 9 lines
compat/fsmonitor/fsm-path-utils-linux.c new file +9 -0
+static struct mntent *find_mount(const char *path, const struct
+statvfs *fs) {  const char *const mounts = "/proc/mounts";  const
+char *rp = real_pathdup(path, 1);  struct mntent *ment = NULL;
+struct mntent *found = NULL;  struct statvfs mntfs;  FILE *fp;  int
+dlen, plen, flen = 0;
+
+ fp = setmntent(mounts, "r");
+ if (!fp) {
+ error_errno(_("setmntent('%s') failed"), mounts); return NULL;

This code would probably be nicer if you returned int, and passed a pointer to a struct to populate as a paremeter. Then you could just "return error..." for this and the below cases.

Quoted 17 lines
compat/fsmonitor/fsm-path-utils-linux.c new file +17 -0
+ }
+
+ plen = strlen(rp);
+
+ /* read all the mount information and compare to path */ while
+ ((ment = getmntent(fp)) != NULL) { if (statvfs(ment->mnt_dir,
+ &mntfs)) { switch (errno) { case EPERM:
+ case ESRCH:
+ case EACCES:
+ continue;
+ default:
+ error_errno(_("statvfs('%s') failed"), ment->mnt_dir);
+ endmntent(fp);is return NULL; } }
+
+ /* is mount on the same filesystem and is a prefix of the path */ if
+ ((fs->f_fsid == mntfs.f_fsid) && !strncmp(ment->mnt_dir, rp,
+ strlen(ment->mnt_dir))) { dlen = strlen(ment->mnt_dir); if (dlen >

compat/fsmonitor/fsm-path-utils-linux.c

Quoted 10 lines
compat/fsmonitor/fsm-path-utils-linux.c new file +10 -0
+ plen) continue;
+ /*
+ * root is always a potential match; otherwise look for
+ * directory prefix
+ */
+ if ((dlen == 1 && ment->mnt_dir[0] == '/') || (dlen > flen &&
+ (!rp[dlen] || rp[dlen] == '/'))) { flen = dlen;
+ /*
+ * https://man7.org/linux/man-pages/man3/getmntent.3.html
+ <https://protect-

us.mimecast.com/s/aOmSCgJyVrT01WlYc76tSR?domain=man

compat/fsmonitor/fsm-path-utils-linux.c line 89

Quoted 7 lines
compat/fsmonitor/fsm-path-utils-linux.c new file +7 -0
+ 7.org>
+ *
+ * The pointer points to a static area of memory which is
+ * overwritten by subsequent calls to getmntent().
+ */
+ if (!found)
+ CALLOC_ARRAY(found, 1);

It seems we never populate >1 of these, so don't you just want xmalloc(). Or actually...

Quoted 8 lines
compat/fsmonitor/fsm-path-utils-linux.c new file +5 -0
+ free(found->mnt_dir);
+ free(found->mnt_type);
+ free(found->mnt_fsname);
+ found->mnt_dir = xmemdupz(ment->mnt_dir, strlen(ment->mnt_dir));
+ found->mnt_type = xmemdupz(ment->mnt_type, strlen(ment-

mnt_type));
+ found->mnt_fsname = xmemdupz(ment->mnt_fsname,
+ found->strlen(ment->mnt_fsname));

Don't mix mem*() and str*(). In this case we need the string to be '\0' delimited, so use xstrndup().

And once we do that, we might wonder why we're explicitly finding the '\0', just to pass it to the xstrn*() function, when we can just do:

found->mnt_dir = xstrdup(ment->mnt_dir);
...

Which would AFAICT be equivalent to what you're doing here.

compat/fsmonitor/fsm-path-utils-linux.c

Quoted 10 lines

+ }
+ }
+ }
+ endmntent(fp);

compat/fsmonitor/fsm-path-utils-linux.c new file +6 -0
+
+ return found;
+}
+
+int fsmonitor__get_fs_info(const char *path, struct fs_info *fs_info)
+{  struct mntent *ment;

...make this (or above) a "struct mntent ment", then pass &ment down, so we can fill that struct? Dunno...

Quoted 14 lines

+ struct statvfs fs;

compat/fsmonitor/fsm-path-utils-linux.c new file +13 -0
+
+ if (statvfs(path, &fs))
+ return error_errno(_("statvfs('%s') failed"), path);
+
+ ment = find_mount(path, &fs);
+ if (!ment)
+ return -1;
+
+ trace_printf_key(&trace_fsmonitor,
+ "statvfs('%s') [flags 0x%08lx] '%s' '%s'", path, fs.f_flag,
+ ment->mnt_type, ment->mnt_fsname);
+
+ if (ME_REMOTE(ment->mnt_fsname, ment->mnt_type)) fs_info-
Quoted 2 lines

is_remote
+ = 1; else fs_info->is_remote = 0;

Shorter:

fs_info->is_remote = ME_REMOTE(ment->mnt_fsname, ment->mnt_type);

#24 lore raw

PATCH v2 1/12 fsmonitor: refactor filesystem checks to common interface

In reply to Eric DeCosta via GitGitGadget

From: Eric DeCosta <[email protected]>

Provide a common interface for getting basic filesystem information including filesystem type and whether the filesystem is remote.

Refactor existing code for getting basic filesystem info and detecting remote file systems to the new interface.

Refactor filesystem checks to leverage new interface. For macOS, error-out if the Unix Domain socket (UDS) file is on a remote filesystem.

---
 Makefile                                 |   1 +
 compat/fsmonitor/fsm-path-utils-darwin.c |  43 ++++++
 compat/fsmonitor/fsm-path-utils-win32.c  | 128 +++++++++++++++++
 compat/fsmonitor/fsm-settings-darwin.c   |  69 +++------
 compat/fsmonitor/fsm-settings-win32.c    | 172 +----------------------
 contrib/buildsystems/CMakeLists.txt      |   2 +
 fsmonitor-path-utils.h                   |  26 ++++
 fsmonitor-settings.c                     |  50 +++++++
 8 files changed, 272 insertions(+), 219 deletions(-)
 create mode 100644 compat/fsmonitor/fsm-path-utils-darwin.c
 create mode 100644 compat/fsmonitor/fsm-path-utils-win32.c
 create mode 100644 fsmonitor-path-utils.h
Makefile +1 -0
index 6bfb62cbe94..99a3d9e6322 100644
@@ -2043,6 +2043,7 @@ endif
 ifdef FSMONITOR_OS_SETTINGS
 	COMPAT_CFLAGS += -DHAVE_FSMONITOR_OS_SETTINGS
 	COMPAT_OBJS += compat/fsmonitor/fsm-settings-$(FSMONITOR_OS_SETTINGS).o
+	COMPAT_OBJS += compat/fsmonitor/fsm-path-utils-$(FSMONITOR_OS_SETTINGS).o
 endif
 
 ifeq ($(TCLTK_PATH),)
compat/fsmonitor/fsm-path-utils-darwin.c new file +43 -0

43 lines not on this page · raw message

compat/fsmonitor/fsm-path-utils-win32.c new file +128 -0

128 lines not on this page · raw message

compat/fsmonitor/fsm-settings-darwin.c +20 -49

101 lines not on this page · raw message

compat/fsmonitor/fsm-settings-win32.c +2 -170

190 lines not on this page · raw message

contrib/buildsystems/CMakeLists.txt +2 -0
index ea2a531be87..5482a04b3ce 100644
@@ -308,6 +308,7 @@ if(SUPPORTS_SIMPLE_IPC)
 		add_compile_definitions(HAVE_FSMONITOR_DAEMON_BACKEND)
 		list(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-win32.c)
 		list(APPEND compat_SOURCES compat/fsmonitor/fsm-health-win32.c)
+		list(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-win32.c)
 
 		add_compile_definitions(HAVE_FSMONITOR_OS_SETTINGS)
 		list(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-win32.c)
@@ -315,6 +316,7 @@ if(SUPPORTS_SIMPLE_IPC)
 		add_compile_definitions(HAVE_FSMONITOR_DAEMON_BACKEND)
 		list(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-darwin.c)
 		list(APPEND compat_SOURCES compat/fsmonitor/fsm-health-darwin.c)
+		list(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-darwin.c)
 
 		add_compile_definitions(HAVE_FSMONITOR_OS_SETTINGS)
 		list(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-darwin.c)
fsmonitor-path-utils.h new file +26 -0
new file mode 100644
index 00000000000..41edf5b934f
@@ -0,0 +1,26 @@
+#ifndef FSM_PATH_UTILS_H
+#define FSM_PATH_UTILS_H
+
+struct fs_info {
+	int is_remote;
+	char *typename;
+};
+
+/*
+ * Get some basic filesystem informtion for the given path
+ *
+ * The caller owns the storage that is occupied by fs_info and
+ * is responsible for releasing it.
+ *
+ * Returns -1 on error, zero otherwise.
+ */
+int fsmonitor__get_fs_info(const char *path, struct fs_info *fs_info);
+
+/*
+ * Determines if the filesystem that path resides on is remote.
+ *
+ * Returns -1 on error, 0 if not remote, 1 if remote.
+ */
+int fsmonitor__is_fs_remote(const char *path);
+
+#endif
fsmonitor-settings.c +50 -0

68 lines not on this page · raw message

Signature · 1 line
gitgitgadget
#25 lore raw

PATCH v2 2/12 fsmonitor: relocate socket file if .git directory is remote

In reply to Eric DeCosta via GitGitGadget

From: Eric DeCosta <[email protected]>

If the .git directory is on a remote filesystem, create the socket file in 'fsmonitor.socketDir' if it is defined, else create it in $HOME.

---
 Makefile                               |  1 +
 builtin/fsmonitor--daemon.c            |  3 +-
 compat/fsmonitor/fsm-ipc-darwin.c      | 52 ++++++++++++++++++++++++++
 compat/fsmonitor/fsm-ipc-win32.c       |  9 +++++
 compat/fsmonitor/fsm-settings-darwin.c |  2 +-
 contrib/buildsystems/CMakeLists.txt    |  2 +
 fsmonitor-ipc.c                        | 18 ++++-----
 fsmonitor-ipc.h                        |  4 +-
 8 files changed, 78 insertions(+), 13 deletions(-)
 create mode 100644 compat/fsmonitor/fsm-ipc-darwin.c
 create mode 100644 compat/fsmonitor/fsm-ipc-win32.c
Makefile +1 -0
index 99a3d9e6322..9553d590824 100644
@@ -2038,6 +2038,7 @@ ifdef FSMONITOR_DAEMON_BACKEND
 	COMPAT_CFLAGS += -DHAVE_FSMONITOR_DAEMON_BACKEND
 	COMPAT_OBJS += compat/fsmonitor/fsm-listen-$(FSMONITOR_DAEMON_BACKEND).o
 	COMPAT_OBJS += compat/fsmonitor/fsm-health-$(FSMONITOR_DAEMON_BACKEND).o
+	COMPAT_OBJS += compat/fsmonitor/fsm-ipc-$(FSMONITOR_DAEMON_BACKEND).o
 endif
 
 ifdef FSMONITOR_OS_SETTINGS
builtin/fsmonitor--daemon.c +2 -1
index c69da93eceb..6e417846d8b 100644
@@ -1343,7 +1343,8 @@ static int fsmonitor_run_daemon(void)
 	 * directory.)
 	 */
 	strbuf_init(&state.path_ipc, 0);
-	strbuf_addstr(&state.path_ipc, absolute_path(fsmonitor_ipc__get_path()));
+	strbuf_addstr(&state.path_ipc,
+		absolute_path(fsmonitor_ipc__get_path(the_repository)));
 
 	/*
 	 * Confirm that we can create platform-specific resources for the
compat/fsmonitor/fsm-ipc-darwin.c new file +52 -0

52 lines not on this page · raw message

compat/fsmonitor/fsm-ipc-win32.c new file +9 -0
new file mode 100644
index 00000000000..e08c505c148
@@ -0,0 +1,9 @@
+#include "config.h"
+#include "fsmonitor-ipc.h"
+
+const char *fsmonitor_ipc__get_path(struct repository *r) {
+	static char *ret;
+	if (!ret)
+		ret = git_pathdup("fsmonitor--daemon.ipc");
+	return ret;
+}
compat/fsmonitor/fsm-settings-darwin.c +1 -1
index 699f0b272e6..7241c4c22c9 100644
@@ -26,7 +26,7 @@
 static enum fsmonitor_reason check_uds_volume(struct repository *r)
 {
 	struct fs_info fs;
-	const char *ipc_path = fsmonitor_ipc__get_path();
+	const char *ipc_path = fsmonitor_ipc__get_path(r);
 	struct strbuf path = STRBUF_INIT;
 	strbuf_add(&path, ipc_path, strlen(ipc_path));
 
contrib/buildsystems/CMakeLists.txt +2 -0
index 5482a04b3ce..787738e6fa3 100644
@@ -308,6 +308,7 @@ if(SUPPORTS_SIMPLE_IPC)
 		add_compile_definitions(HAVE_FSMONITOR_DAEMON_BACKEND)
 		list(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-win32.c)
 		list(APPEND compat_SOURCES compat/fsmonitor/fsm-health-win32.c)
+		list(APPEND compat_SOURCES compat/fsmonitor/fsm-ipc-win32.c)
 		list(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-win32.c)
 
 		add_compile_definitions(HAVE_FSMONITOR_OS_SETTINGS)
@@ -316,6 +317,7 @@ if(SUPPORTS_SIMPLE_IPC)
 		add_compile_definitions(HAVE_FSMONITOR_DAEMON_BACKEND)
 		list(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-darwin.c)
 		list(APPEND compat_SOURCES compat/fsmonitor/fsm-health-darwin.c)
+		list(APPEND compat_SOURCES compat/fsmonitor/fsm-ipc-darwin.c)
 		list(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-darwin.c)
 
 		add_compile_definitions(HAVE_FSMONITOR_OS_SETTINGS)
fsmonitor-ipc.c +8 -10
index 789e7397baa..c0f42301c84 100644
@@ -18,7 +18,7 @@ int fsmonitor_ipc__is_supported(void)
 	return 0;
 }
 
-const char *fsmonitor_ipc__get_path(void)
+const char *fsmonitor_ipc__get_path(struct repository *r)
 {
 	return NULL;
 }
@@ -47,11 +47,9 @@ int fsmonitor_ipc__is_supported(void)
 	return 1;
 }
 
-GIT_PATH_FUNC(fsmonitor_ipc__get_path, "fsmonitor--daemon.ipc")
-
 enum ipc_active_state fsmonitor_ipc__get_state(void)
 {
-	return ipc_get_active_state(fsmonitor_ipc__get_path());
+	return ipc_get_active_state(fsmonitor_ipc__get_path(the_repository));
 }
 
 static int spawn_daemon(void)
@@ -81,8 +79,8 @@ int fsmonitor_ipc__send_query(const char *since_token,
 	trace2_data_string("fsm_client", NULL, "query/command", tok);
 
 try_again:
-	state = ipc_client_try_connect(fsmonitor_ipc__get_path(), &options,
-				       &connection);
+	state = ipc_client_try_connect(fsmonitor_ipc__get_path(the_repository),
+						&options, &connection);
 
 	switch (state) {
 	case IPC_STATE__LISTENING:
@@ -117,13 +115,13 @@ try_again:
 
 	case IPC_STATE__INVALID_PATH:
 		ret = error(_("fsmonitor_ipc__send_query: invalid path '%s'"),
-			    fsmonitor_ipc__get_path());
+			    fsmonitor_ipc__get_path(the_repository));
 		goto done;
 
 	case IPC_STATE__OTHER_ERROR:
 	default:
 		ret = error(_("fsmonitor_ipc__send_query: unspecified error on '%s'"),
-			    fsmonitor_ipc__get_path());
+			    fsmonitor_ipc__get_path(the_repository));
 		goto done;
 	}
 
@@ -149,8 +147,8 @@ int fsmonitor_ipc__send_command(const char *command,
 	options.wait_if_busy = 1;
 	options.wait_if_not_found = 0;
 
-	state = ipc_client_try_connect(fsmonitor_ipc__get_path(), &options,
-				       &connection);
+	state = ipc_client_try_connect(fsmonitor_ipc__get_path(the_repository),
+						&options, &connection);
 	if (state != IPC_STATE__LISTENING) {
 		die(_("fsmonitor--daemon is not running"));
 		return -1;
fsmonitor-ipc.h +3 -1
index b6a7067c3af..8b489da762b 100644
@@ -3,6 +3,8 @@
 
 #include "simple-ipc.h"
 
+struct repository;
+
 /*
  * Returns true if built-in file system monitor daemon is defined
  * for this platform.
@@ -16,7 +18,7 @@ int fsmonitor_ipc__is_supported(void);
  *
  * Returns NULL if the daemon is not supported on this platform.
  */
-const char *fsmonitor_ipc__get_path(void);
+const char *fsmonitor_ipc__get_path(struct repository *r);
 
 /*
  * Try to determine whether there is a `git-fsmonitor--daemon` process
Signature · 1 line
gitgitgadget
#23 lore raw

v2 cover fsmonitor: Implement fsmonitor for Linux

In reply to Eric DeCosta via GitGitGadget

Goal is to deliver fsmonitor for Linux that is on par with fsmonitor for Windows and Mac OS.

This patch set builds upon previous work for done for Windows and Mac OS (first 6 patches) to implement a fsmonitor back-end for Linux based on the Linux inotify API. inotify differs significantly from the equivalent Windows and Mac OS APIs in that a watch must be registered for every directory of interest (rather than a singular watch at the root of the directory tree) and special care must be taken to handle directory renames correctly.

More information about inotify:
https://man7.org/linux/man-pages/man7/inotify.7.html

v1 differs from v0:

* Code review feedback
* Update how and which code can be shared between Mac OS and Linux
* Increase polling frequency to every 1ms (matches Mac OS)
* Updates to t7527 to improve test stability

Eric DeCosta (12):
fsmonitor: refactor filesystem checks to common interface
fsmonitor: relocate socket file if .git directory is remote
fsmonitor: avoid socket location check if using hook
fsmonitor: deal with synthetic firmlinks on macOS
fsmonitor: check for compatability before communicating with fsmonitor
fsmonitor: add documentation for allowRemote and socketDir options
fsmonitor: prepare to share code between Mac OS and Linux
fsmonitor: determine if filesystem is local or remote
fsmonitor: implement filesystem change listener for Linux
fsmonitor: enable fsmonitor for Linux
fsmonitor: test updates
fsmonitor: update doc for Linux

 Documentation/config.txt                   |   2 +
 Documentation/config/fsmonitor--daemon.txt |  11 +
 Documentation/git-fsmonitor--daemon.txt    |  45 +-
 Makefile                                   |   9 +
 builtin/fsmonitor--daemon.c                |  11 +-
 compat/fsmonitor/fsm-health-linux.c        |  24 +
 compat/fsmonitor/fsm-ipc-unix.c            |  52 ++
 compat/fsmonitor/fsm-ipc-win32.c           |   9 +
 compat/fsmonitor/fsm-listen-darwin.c       |  14 +-
 compat/fsmonitor/fsm-listen-linux.c        | 664 +++++++++++++++++++++
 compat/fsmonitor/fsm-path-utils-darwin.c   | 135 +++++
 compat/fsmonitor/fsm-path-utils-linux.c    | 169 ++++++
 compat/fsmonitor/fsm-path-utils-win32.c    | 145 +++++
 compat/fsmonitor/fsm-settings-darwin.c     |  90 +--
 compat/fsmonitor/fsm-settings-linux.c      |  11 +
 compat/fsmonitor/fsm-settings-unix.c       |  62 ++
 compat/fsmonitor/fsm-settings-unix.h       |  11 +
 compat/fsmonitor/fsm-settings-win32.c      | 174 +-----
 config.mak.uname                           |  10 +
 contrib/buildsystems/CMakeLists.txt        |  19 +-
 fsmonitor--daemon.h                        |   3 +
 fsmonitor-ipc.c                            |  18 +-
 fsmonitor-ipc.h                            |   4 +-
 fsmonitor-path-utils.h                     |  60 ++
 fsmonitor-settings.c                       |  68 ++-
 fsmonitor-settings.h                       |   4 +-
 fsmonitor.c                                |   7 +
 t/t7527-builtin-fsmonitor.sh               |  72 ++-
 28 files changed, 1606 insertions(+), 297 deletions(-)
 create mode 100644 Documentation/config/fsmonitor--daemon.txt
 create mode 100644 compat/fsmonitor/fsm-health-linux.c
 create mode 100644 compat/fsmonitor/fsm-ipc-unix.c
 create mode 100644 compat/fsmonitor/fsm-ipc-win32.c
 create mode 100644 compat/fsmonitor/fsm-listen-linux.c
 create mode 100644 compat/fsmonitor/fsm-path-utils-darwin.c
 create mode 100644 compat/fsmonitor/fsm-path-utils-linux.c
 create mode 100644 compat/fsmonitor/fsm-path-utils-win32.c
 create mode 100644 compat/fsmonitor/fsm-settings-linux.c
 create mode 100644 compat/fsmonitor/fsm-settings-unix.c
 create mode 100644 compat/fsmonitor/fsm-settings-unix.h
 create mode 100644 fsmonitor-path-utils.h

base-commit: d420dda0576340909c3faff364cfbd1485f70376
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-1352%2Fedecosta-mw%2Ffsmonitor_linux-v2
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-1352/edecosta-mw/fsmonitor_linux-v2
Pull-Request: https://github.com/git/git/pull/1352

Range-diff vs v1:

  1:  ec49a74086d =  1:  cd46bed37c3 fsmonitor: refactor filesystem checks to common interface
  2:  7bf1cdfe3b2 =  2:  21d114bda4b fsmonitor: relocate socket file if .git directory is remote
  3:  c5e8b6cfe5d =  3:  664259ed57a fsmonitor: avoid socket location check if using hook
  4:  863063aefee =  4:  d8f20032d6b fsmonitor: deal with synthetic firmlinks on macOS
  5:  fa974bfd5ef =  5:  ab1e0ab967c fsmonitor: check for compatability before communicating with fsmonitor
  6:  af7309745f7 =  6:  9c552239b57 fsmonitor: add documentation for allowRemote and socketDir options
  7:  c085fc15b31 !  7:  295beb89ab1 fsmonitor: prepare to share code between Mac OS and Linux
     @@ Commit message
          fsmonitor: prepare to share code between Mac OS and Linux
      
          Linux and Mac OS can share some of the code originally developed for Mac OS.
     -    Rename the shared code from compat/fsmonitor/fsm-*-dawrin.c to
     -    compat/fsmonitor/fsm-*-unix.c
     -
     -    Update the build to enable sharing of the fsm-*-unix.c files.
      
          Minor update to compat/fsmonitor/fsm-ipc-unix.c to make it cross-platform.
     +    Mac OS and Linux can share fsm-ipc-unix.c
     +
     +    Both platforms can also share compat/fsmonitor/fsm-settings-unix.c but we
     +    will leave room for future, platform-specific checks by having the platform-
     +    specific implementations call into fsm-settings-unix.
      
          Signed-off-by: Eric DeCosta <[email protected]>
      
       ## Makefile ##
      @@ Makefile: endif
     + 
       ifdef FSMONITOR_DAEMON_BACKEND
       	COMPAT_CFLAGS += -DHAVE_FSMONITOR_DAEMON_BACKEND
     ++	ifdef FSMONITOR_DAEMON_COMMON
     ++		COMPAT_OBJS += compat/fsmonitor/fsm-ipc-$(FSMONITOR_DAEMON_COMMON).o
     ++	else
     ++		COMPAT_OBJS += compat/fsmonitor/fsm-ipc-$(FSMONITOR_DAEMON_BACKEND).o
     ++	endif
       	COMPAT_OBJS += compat/fsmonitor/fsm-listen-$(FSMONITOR_DAEMON_BACKEND).o
     --	COMPAT_OBJS += compat/fsmonitor/fsm-health-$(FSMONITOR_DAEMON_BACKEND).o
     + 	COMPAT_OBJS += compat/fsmonitor/fsm-health-$(FSMONITOR_DAEMON_BACKEND).o
      -	COMPAT_OBJS += compat/fsmonitor/fsm-ipc-$(FSMONITOR_DAEMON_BACKEND).o
     -+	COMPAT_OBJS += compat/fsmonitor/fsm-health-$(FSMONITOR_DAEMON_COMMON).o
     -+	COMPAT_OBJS += compat/fsmonitor/fsm-ipc-$(FSMONITOR_DAEMON_COMMON).o
       endif
       
       ifdef FSMONITOR_OS_SETTINGS
       	COMPAT_CFLAGS += -DHAVE_FSMONITOR_OS_SETTINGS
     --	COMPAT_OBJS += compat/fsmonitor/fsm-settings-$(FSMONITOR_OS_SETTINGS).o
     -+	COMPAT_OBJS += compat/fsmonitor/fsm-settings-$(FSMONITOR_DAEMON_COMMON).o
     ++	ifdef FSMONITOR_DAEMON_COMMON
     ++		COMPAT_OBJS += compat/fsmonitor/fsm-settings-$(FSMONITOR_DAEMON_COMMON).o
     ++	endif
     + 	COMPAT_OBJS += compat/fsmonitor/fsm-settings-$(FSMONITOR_OS_SETTINGS).o
       	COMPAT_OBJS += compat/fsmonitor/fsm-path-utils-$(FSMONITOR_OS_SETTINGS).o
       endif
     - 
      
     - ## compat/fsmonitor/fsm-health-darwin.c => compat/fsmonitor/fsm-health-unix.c ##
     + ## compat/fsmonitor/fsm-health-linux.c (new) ##
     +@@
     ++#include "cache.h"
     ++#include "config.h"
     ++#include "fsmonitor.h"
     ++#include "fsm-health.h"
     ++#include "fsmonitor--daemon.h"
     ++
     ++int fsm_health__ctor(struct fsmonitor_daemon_state *state)
     ++{
     ++	return 0;
     ++}
     ++
     ++void fsm_health__dtor(struct fsmonitor_daemon_state *state)
     ++{
     ++	return;
     ++}
     ++
     ++void fsm_health__loop(struct fsmonitor_daemon_state *state)
     ++{
     ++	return;
     ++}
     ++
     ++void fsm_health__stop_async(struct fsmonitor_daemon_state *state)
     ++{
     ++}
      
       ## compat/fsmonitor/fsm-ipc-darwin.c => compat/fsmonitor/fsm-ipc-unix.c ##
      @@ compat/fsmonitor/fsm-ipc-unix.c: static GIT_PATH_FUNC(fsmonitor_ipc__get_default_path, "fsmonitor--daemon.ipc")
     @@ compat/fsmonitor/fsm-ipc-unix.c: const char *fsmonitor_ipc__get_path(struct repo
       	repo_config_get_string(r, "fsmonitor.socketdir", &sock_dir);
       
      
     - ## compat/fsmonitor/fsm-settings-darwin.c => compat/fsmonitor/fsm-settings-unix.c ##
     + ## compat/fsmonitor/fsm-settings-darwin.c ##
     +@@
     + #include "config.h"
     + #include "fsmonitor.h"
     + #include "fsmonitor-ipc.h"
     +-#include "fsmonitor-settings.h"
     + #include "fsmonitor-path-utils.h"
     +-
     +- /*
     +- * For the builtin FSMonitor, we create the Unix domain socket for the
     +- * IPC in the .git directory.  If the working directory is remote,
     +- * then the socket will be created on the remote file system.  This
     +- * can fail if the remote file system does not support UDS file types
     +- * (e.g. smbfs to a Windows server) or if the remote kernel does not
     +- * allow a non-local process to bind() the socket.  (These problems
     +- * could be fixed by moving the UDS out of the .git directory and to a
     +- * well-known local directory on the client machine, but care should
     +- * be taken to ensure that $HOME is actually local and not a managed
     +- * file share.)
     +- *
     +- * FAT32 and NTFS working directories are problematic too.
     +- *
     +- * The builtin FSMonitor uses a Unix domain socket in the .git
     +- * directory for IPC.  These Windows drive formats do not support
     +- * Unix domain sockets, so mark them as incompatible for the daemon.
     +- *
     +- */
     +-static enum fsmonitor_reason check_uds_volume(struct repository *r)
     +-{
     +-	struct fs_info fs;
     +-	const char *ipc_path = fsmonitor_ipc__get_path(r);
     +-	struct strbuf path = STRBUF_INIT;
     +-	strbuf_add(&path, ipc_path, strlen(ipc_path));
     +-
     +-	if (fsmonitor__get_fs_info(dirname(path.buf), &fs) == -1) {
     +-		strbuf_release(&path);
     +-		return FSMONITOR_REASON_ERROR;
     +-	}
     +-
     +-	strbuf_release(&path);
     +-
     +-	if (fs.is_remote ||
     +-		!strcmp(fs.typename, "msdos") ||
     +-		!strcmp(fs.typename, "ntfs")) {
     +-		free(fs.typename);
     +-		return FSMONITOR_REASON_NOSOCKETS;
     +-	}
     +-
     +-	free(fs.typename);
     +-	return FSMONITOR_REASON_OK;
     +-}
     ++#include "fsmonitor-settings.h"
     ++#include "fsm-settings-unix.h"
     + 
     + enum fsmonitor_reason fsm_os__incompatible(struct repository *r, int ipc)
     + {
     +-	enum fsmonitor_reason reason;
     +-
     +-	if (ipc) {
     +-		reason = check_uds_volume(r);
     +-		if (reason != FSMONITOR_REASON_OK)
     +-			return reason;
     +-	}
     +-
     +-	return FSMONITOR_REASON_OK;
     ++    return fsm_os__incompatible_unix(r, ipc);
     + }
     +
     + ## compat/fsmonitor/fsm-settings-linux.c (new) ##
     +@@
     ++#include "config.h"
     ++#include "fsmonitor.h"
     ++#include "fsmonitor-ipc.h"
     ++#include "fsmonitor-path-utils.h"
     ++#include "fsmonitor-settings.h"
     ++#include "fsm-settings-unix.h"
     ++
     ++enum fsmonitor_reason fsm_os__incompatible(struct repository *r, int ipc)
     ++{
     ++    return fsm_os__incompatible_unix(r, ipc);
     ++}
      
     - ## config.mak.uname ##
     -@@ config.mak.uname: ifeq ($(uname_S),Linux)
     - 	ifneq ($(findstring .el7.,$(uname_R)),)
     - 		BASIC_CFLAGS += -std=c99
     - 	endif
     + ## compat/fsmonitor/fsm-settings-unix.c (new) ##
     +@@
     ++#include "config.h"
     ++#include "fsmonitor.h"
     ++#include "fsmonitor-ipc.h"
     ++#include "fsmonitor-path-utils.h"
     ++#include "fsm-settings-unix.h"
      +
     - endif
     - ifeq ($(uname_S),GNU/kFreeBSD)
     - 	HAVE_ALLOCA_H = YesPlease
     ++ /*
     ++ * For the builtin FSMonitor, we create the Unix domain socket for the
     ++ * IPC in the .git directory.  If the working directory is remote,
     ++ * then the socket will be created on the remote file system.  This
     ++ * can fail if the remote file system does not support UDS file types
     ++ * (e.g. smbfs to a Windows server) or if the remote kernel does not
     ++ * allow a non-local process to bind() the socket.  (These problems
     ++ * could be fixed by moving the UDS out of the .git directory and to a
     ++ * well-known local directory on the client machine, but care should
     ++ * be taken to ensure that $HOME is actually local and not a managed
     ++ * file share.)
     ++ *
     ++ * FAT32 and NTFS working directories are problematic too.
     ++ *
     ++ * The builtin FSMonitor uses a Unix domain socket in the .git
     ++ * directory for IPC.  These Windows drive formats do not support
     ++ * Unix domain sockets, so mark them as incompatible for the daemon.
     ++ *
     ++ */
     ++static enum fsmonitor_reason check_uds_volume(struct repository *r)
     ++{
     ++	struct fs_info fs;
     ++	const char *ipc_path = fsmonitor_ipc__get_path(r);
     ++	struct strbuf path = STRBUF_INIT;
     ++	strbuf_add(&path, ipc_path, strlen(ipc_path));
     ++
     ++	if (fsmonitor__get_fs_info(dirname(path.buf), &fs) == -1) {
     ++		strbuf_release(&path);
     ++		return FSMONITOR_REASON_ERROR;
     ++	}
     ++
     ++	strbuf_release(&path);
     ++
     ++	if (fs.is_remote ||
     ++		!strcmp(fs.typename, "msdos") ||
     ++		!strcmp(fs.typename, "ntfs")) {
     ++		free(fs.typename);
     ++		return FSMONITOR_REASON_NOSOCKETS;
     ++	}
     ++
     ++	free(fs.typename);
     ++	return FSMONITOR_REASON_OK;
     ++}
     ++
     ++enum fsmonitor_reason fsm_os__incompatible_unix(struct repository *r, int ipc)
     ++{
     ++	enum fsmonitor_reason reason;
     ++
     ++	if (ipc) {
     ++		reason = check_uds_volume(r);
     ++		if (reason != FSMONITOR_REASON_OK)
     ++			return reason;
     ++	}
     ++
     ++	return FSMONITOR_REASON_OK;
     ++}
     +
     + ## compat/fsmonitor/fsm-settings-unix.h (new) ##
     +@@
     ++#ifndef FSM_SETTINGS_UNIX_H
     ++#define FSM_SETTINGS_UNIX_H
     ++
     ++#ifdef HAVE_FSMONITOR_OS_SETTINGS
     ++/*
     ++ * Check for compatibility on unix-like systems (e.g. Darwin and Linux)
     ++ */
     ++enum fsmonitor_reason fsm_os__incompatible_unix(struct repository *r, int ipc);
     ++#endif /* HAVE_FSMONITOR_OS_SETTINGS */
     ++
     ++#endif /* FSM_SETTINGS_UNIX_H */
     +
     + ## config.mak.uname ##
      @@ config.mak.uname: ifeq ($(uname_S),Darwin)
       	ifndef NO_UNIX_SOCKETS
       	FSMONITOR_DAEMON_BACKEND = darwin
     @@ config.mak.uname: ifeq ($(uname_S),Darwin)
       	endif
       	endif
       
     -@@ config.mak.uname: ifeq ($(uname_S),Windows)
     - 	# support it.
     - 	FSMONITOR_DAEMON_BACKEND = win32
     - 	FSMONITOR_OS_SETTINGS = win32
     -+	FSMONITOR_DAEMON_COMMON = win32
     - 
     - 	NO_SVN_TESTS = YesPlease
     - 	RUNTIME_PREFIX = YesPlease
     -@@ config.mak.uname: ifeq ($(uname_S),MINGW)
     - 	# support it.
     - 	FSMONITOR_DAEMON_BACKEND = win32
     - 	FSMONITOR_OS_SETTINGS = win32
     -+	FSMONITOR_DAEMON_COMMON = win32
     - 
     - 	RUNTIME_PREFIX = YesPlease
     - 	HAVE_WPGMPTR = YesWeDo
      
       ## contrib/buildsystems/CMakeLists.txt ##
     +@@ contrib/buildsystems/CMakeLists.txt: else()
     + endif()
     + 
     + if(SUPPORTS_SIMPLE_IPC)
     +-	if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
     ++	if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
     ++		add_compile_definitions(HAVE_FSMONITOR_DAEMON_BACKEND)
     ++		list(APPEND compat_SOURCES compat/fsmonitor/fsm-ipc-unix.c)
     ++		list(APPEND compat_SOURCES compat/fsmonitor/fsm-health-linux.c)
     ++		list(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-linux.c)
     ++		list(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-linux.c)
     ++
     ++		add_compile_definitions(HAVE_FSMONITOR_OS_SETTINGS)
     ++		list(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-unix.c)
     ++		list(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-linux.c)
     ++	elseif(CMAKE_SYSTEM_NAME STREQUAL "Windows")
     + 		add_compile_definitions(HAVE_FSMONITOR_DAEMON_BACKEND)
     + 		list(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-win32.c)
     + 		list(APPEND compat_SOURCES compat/fsmonitor/fsm-health-win32.c)
      @@ contrib/buildsystems/CMakeLists.txt: if(SUPPORTS_SIMPLE_IPC)
       		list(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-win32.c)
       	elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
       		add_compile_definitions(HAVE_FSMONITOR_DAEMON_BACKEND)
     +-		list(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-darwin.c)
      +		list(APPEND compat_SOURCES compat/fsmonitor/fsm-ipc-unix.c)
     -+		list(APPEND compat_SOURCES compat/fsmonitor/fsm-health-unix.c)
     - 		list(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-darwin.c)
     --		list(APPEND compat_SOURCES compat/fsmonitor/fsm-health-darwin.c)
     + 		list(APPEND compat_SOURCES compat/fsmonitor/fsm-health-darwin.c)
      -		list(APPEND compat_SOURCES compat/fsmonitor/fsm-ipc-darwin.c)
     ++		list(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-darwin.c)
       		list(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-darwin.c)
       
       		add_compile_definitions(HAVE_FSMONITOR_OS_SETTINGS)
     --		list(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-darwin.c)
      +		list(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-unix.c)
     + 		list(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-darwin.c)
       	endif()
       endif()
     - 
  8:  5ecbb3082f1 !  8:  7d7ef78728f fsmonitor: determine if filesystem is local or remote
     @@ compat/fsmonitor/fsm-path-utils-linux.c (new)
      +	 || strcmp ("-hosts", Fs_name) == 0)
      +#endif
      +
     -+static struct mntent *find_mount(const char *path, const struct statvfs *fs)
     ++static int find_mount(const char *path, const struct statvfs *fs,
     ++	struct mntent *ent)
      +{
      +	const char *const mounts = "/proc/mounts";
      +	const char *rp = real_pathdup(path, 1);
      +	struct mntent *ment = NULL;
     -+	struct mntent *found = NULL;
      +	struct statvfs mntfs;
      +	FILE *fp;
     ++	int found = 0;
      +	int dlen, plen, flen = 0;
      +
     ++	ent->mnt_fsname = NULL;
     ++	ent->mnt_dir = NULL;
     ++	ent->mnt_type = NULL;
     ++
      +	fp = setmntent(mounts, "r");
      +	if (!fp) {
      +		error_errno(_("setmntent('%s') failed"), mounts);
     -+		return NULL;
     ++		return -1;
      +	}
      +
      +	plen = strlen(rp);
     @@ compat/fsmonitor/fsm-path-utils-linux.c (new)
      +			default:
      +				error_errno(_("statvfs('%s') failed"), ment->mnt_dir);
      +				endmntent(fp);
     -+				return NULL;
     ++				return -1;
      +			}
      +		}
      +
     @@ compat/fsmonitor/fsm-path-utils-linux.c (new)
      +				 * The pointer points to a static area of memory which is
      +				 * overwritten by subsequent calls to getmntent().
      +				 */
     -+				if (!found)
     -+					CALLOC_ARRAY(found, 1);
     -+				free(found->mnt_dir);
     -+				free(found->mnt_type);
     -+				free(found->mnt_fsname);
     -+				found->mnt_dir = xmemdupz(ment->mnt_dir, strlen(ment->mnt_dir));
     -+				found->mnt_type = xmemdupz(ment->mnt_type, strlen(ment->mnt_type));
     -+				found->mnt_fsname = xmemdupz(ment->mnt_fsname, strlen(ment->mnt_fsname));
     ++				found = 1;
     ++				free(ent->mnt_fsname);
     ++				free(ent->mnt_dir);
     ++				free(ent->mnt_type);
     ++				ent->mnt_fsname = xstrdup(ment->mnt_fsname);
     ++				ent->mnt_dir = xstrdup(ment->mnt_dir);
     ++				ent->mnt_type = xstrdup(ment->mnt_type);
      +			}
      +		}
      +	}
      +	endmntent(fp);
      +
     -+	return found;
     ++	if (!found)
     ++		return -1;
     ++
     ++	return 0;
      +}
      +
      +int fsmonitor__get_fs_info(const char *path, struct fs_info *fs_info)
      +{
     -+	struct mntent *ment;
     ++	struct mntent ment;
      +	struct statvfs fs;
      +
      +	if (statvfs(path, &fs))
      +		return error_errno(_("statvfs('%s') failed"), path);
      +
     -+	ment = find_mount(path, &fs);
     -+	if (!ment)
     ++
     ++	if (find_mount(path, &fs, &ment) < 0) {
     ++		free(ment.mnt_fsname);
     ++		free(ment.mnt_dir);
     ++		free(ment.mnt_type);
      +		return -1;
     ++	}
      +
      +	trace_printf_key(&trace_fsmonitor,
      +			 "statvfs('%s') [flags 0x%08lx] '%s' '%s'",
     -+			 path, fs.f_flag, ment->mnt_type, ment->mnt_fsname);
     -+
     -+	if (ME_REMOTE(ment->mnt_fsname, ment->mnt_type))
     -+		fs_info->is_remote = 1;
     -+	else
     -+		fs_info->is_remote = 0;
     ++			 path, fs.f_flag, ment.mnt_type, ment.mnt_fsname);
      +
     -+	fs_info->typename = ment->mnt_fsname;
     -+	free(ment->mnt_dir);
     -+	free(ment->mnt_type);
     -+	free(ment);
     ++	fs_info->is_remote = ME_REMOTE(ment.mnt_fsname, ment.mnt_type);
     ++	fs_info->typename = ment.mnt_fsname;
     ++	free(ment.mnt_dir);
     ++	free(ment.mnt_type);
      +
      +	trace_printf_key(&trace_fsmonitor,
      +				"'%s' is_remote: %d",
  9:  7465fe7a8b4 !  9:  4f9c5358475 fsmonitor: implement filesystem change listener for Linux
     @@ compat/fsmonitor/fsm-listen-linux.c (new)
      +
      +/*
      + * Non-blocking read of the inotify events stream. The inotify fd is polled
     -+ * every few millisec to help minimize the number of queue overflows.
     ++ * frequently to help minimize the number of queue overflows.
      + */
      +void fsm_listen__loop(struct fsmonitor_daemon_state *state)
      +{
     @@ compat/fsmonitor/fsm-listen-linux.c (new)
      +	for(;;) {
      +		switch (state->listen_data->shutdown) {
      +			case SHUTDOWN_CONTINUE:
     -+				poll_num = poll(fds, 1, 10);
     ++				poll_num = poll(fds, 1, 1);
      +				if (poll_num == -1) {
      +					if (errno == EINTR)
      +						continue;

10: 3a2db9aa076 ! 10: 07650ecd27b fsmonitor: enable fsmonitor for Linux
@@ Metadata
## Commit message ##
fsmonitor: enable fsmonitor for Linux

- Uodate build to enable fsmonitor for Linux.
+ Update build to enable fsmonitor for Linux.

## config.mak.uname ##
@@ config.mak.uname: ifeq ($(uname_S),Linux)
+ ifneq ($(findstring .el7.,$(uname_R)),)
BASIC_CFLAGS += -std=c99
endif
-
+ # The builtin FSMonitor on Linux builds upon Simple-IPC. Both require
+ # Unix domain sockets and PThreads.
+ ifndef NO_PTHREADS
@@ config.mak.uname: ifeq ($(uname_S),Linux)
endif
ifeq ($(uname_S),GNU/kFreeBSD)
HAVE_ALLOCA_H = YesPlease
-
- ## contrib/buildsystems/CMakeLists.txt ##
-@@ contrib/buildsystems/CMakeLists.txt: else()
- endif()
-
- if(SUPPORTS_SIMPLE_IPC)
-- if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
-+ if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
-+ add_compile_definitions(HAVE_FSMONITOR_DAEMON_BACKEND)
-+ list(APPEND compat_SOURCES compat/fsmonitor/fsm-ipc-unix.c)
-+ list(APPEND compat_SOURCES compat/fsmonitor/fsm-health-unix.c)
-+ list(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-linux.c)
-+ list(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-linux.c)
-+
-+ add_compile_definitions(HAVE_FSMONITOR_OS_SETTINGS)
-+ list(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-unix.c)
-+ elseif(CMAKE_SYSTEM_NAME STREQUAL "Windows")
- add_compile_definitions(HAVE_FSMONITOR_DAEMON_BACKEND)
- list(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-win32.c)
- list(APPEND compat_SOURCES compat/fsmonitor/fsm-health-win32.c)
11: 743bdacded5 ! 11: 6682938fff8 fsmonitor: test updates
@@ t/t7527-builtin-fsmonitor.sh: test_expect_success 'edit some files' '

test-tool fsmonitor-client query --token 0 &&

++ test_might_fail git fsmonitor--daemon stop &&
++
+ grep "^event: dir1/modified$" .git/trace &&
+ grep "^event: dir2/modified$" .git/trace &&
+ grep "^event: modified$" .git/trace &&
@@ t/t7527-builtin-fsmonitor.sh: test_expect_success 'create some files' '

start_daemon --tf "$PWD/.git/trace" &&
@@ t/t7527-builtin-fsmonitor.sh: test_expect_success 'create some files' '

test-tool fsmonitor-client query --token 0 &&

++ test_might_fail git fsmonitor--daemon stop &&
++
+ grep "^event: dir1/new$" .git/trace &&
+ grep "^event: dir2/new$" .git/trace &&
+ grep "^event: new$" .git/trace
@@ t/t7527-builtin-fsmonitor.sh: test_expect_success 'delete some files' '

start_daemon --tf "$PWD/.git/trace" &&
@@ t/t7527-builtin-fsmonitor.sh: test_expect_success 'delete some files' '

test-tool fsmonitor-client query --token 0 &&

++ test_might_fail git fsmonitor--daemon stop &&
++
+ grep "^event: dir1/delete$" .git/trace &&
+ grep "^event: dir2/delete$" .git/trace &&
+ grep "^event: delete$" .git/trace
@@ t/t7527-builtin-fsmonitor.sh: test_expect_success 'rename some files' '

start_daemon --tf "$PWD/.git/trace" &&
@@ t/t7527-builtin-fsmonitor.sh: test_expect_success 'rename some files' '

test-tool fsmonitor-client query --token 0 &&

++ test_might_fail git fsmonitor--daemon stop &&
++
+ grep "^event: dir1/rename$" .git/trace &&
+ grep "^event: dir2/rename$" .git/trace &&
+ grep "^event: rename$" .git/trace &&
@@ t/t7527-builtin-fsmonitor.sh: test_expect_success 'rename directory' '

start_daemon --tf "$PWD/.git/trace" &&
@@ t/t7527-builtin-fsmonitor.sh: test_expect_success 'rename directory' '

test-tool fsmonitor-client query --token 0 &&

++ test_might_fail git fsmonitor--daemon stop &&
++
+ grep "^event: dirtorename/*$" .git/trace &&
+ grep "^event: dirrenamed/*$" .git/trace
+ '
@@ t/t7527-builtin-fsmonitor.sh: test_expect_success 'file changes to directory' '

start_daemon --tf "$PWD/.git/trace" &&
@@ t/t7527-builtin-fsmonitor.sh: test_expect_success 'file changes to directory' '

test-tool fsmonitor-client query --token 0 &&

++ test_might_fail git fsmonitor--daemon stop &&
++
+ grep "^event: delete$" .git/trace &&
+ grep "^event: delete/new$" .git/trace
+ '
@@ t/t7527-builtin-fsmonitor.sh: test_expect_success 'directory changes to a file' '

start_daemon --tf "$PWD/.git/trace" &&
@@ t/t7527-builtin-fsmonitor.sh: test_expect_success 'directory changes to a file'

test-tool fsmonitor-client query --token 0 &&

++ test_might_fail git fsmonitor--daemon stop &&
++
+ grep "^event: dir1$" .git/trace
+ '
+
@@ t/t7527-builtin-fsmonitor.sh: test_expect_success 'flush cached data' '
test-tool -C test_flush fsmonitor-client query --token "builtin:test_00000002:0" >actual_2 && nul_to_q <actual_2 >actual_q2 &&
12: 77ed35b3b80 = 12: dd73e126810 fsmonitor: update doc for Linux

Signature · 1 line
gitgitgadget
#29 lore raw

PATCH v2 5/12 fsmonitor: check for compatability before communicating with fsmonitor

From: Eric DeCosta <[email protected]>

If fsmonitor is not in a compatible state, warn with an appropriate message.

---
 fsmonitor-settings.c | 10 +++++++---
 fsmonitor-settings.h |  2 +-
 fsmonitor.c          |  7 +++++++
 3 files changed, 15 insertions(+), 4 deletions(-)
fsmonitor-settings.c +7 -3
index 531a1b6f956..ee63a97dc51 100644
@@ -1,6 +1,7 @@
 #include "cache.h"
 #include "config.h"
 #include "repository.h"
+#include "fsmonitor-ipc.h"
 #include "fsmonitor-settings.h"
 #include "fsmonitor-path-utils.h"
 
@@ -242,10 +243,11 @@ enum fsmonitor_reason fsm_settings__get_reason(struct repository *r)
 	return r->settings.fsmonitor->reason;
 }
 
-char *fsm_settings__get_incompatible_msg(const struct repository *r,
+char *fsm_settings__get_incompatible_msg(struct repository *r,
 					 enum fsmonitor_reason reason)
 {
 	struct strbuf msg = STRBUF_INIT;
+	const char *socket_dir;
 
 	switch (reason) {
 	case FSMONITOR_REASON_UNTESTED:
@@ -281,9 +283,11 @@ char *fsm_settings__get_incompatible_msg(const struct repository *r,
 		goto done;
 
 	case FSMONITOR_REASON_NOSOCKETS:
+		socket_dir = dirname((char *)fsmonitor_ipc__get_path(r));
 		strbuf_addf(&msg,
-			    _("repository '%s' is incompatible with fsmonitor due to lack of Unix sockets"),
-			    r->worktree);
+			    _("socket directory '%s' is incompatible with fsmonitor due"
+			      " to lack of Unix sockets support"),
+			    socket_dir);
 		goto done;
 	}
 
fsmonitor-settings.h +1 -1
index 0721617b95a..ab02e3995ee 100644
@@ -33,7 +33,7 @@ enum fsmonitor_mode fsm_settings__get_mode(struct repository *r);
 const char *fsm_settings__get_hook_path(struct repository *r);
 
 enum fsmonitor_reason fsm_settings__get_reason(struct repository *r);
-char *fsm_settings__get_incompatible_msg(const struct repository *r,
+char *fsm_settings__get_incompatible_msg(struct repository *r,
 					 enum fsmonitor_reason reason);
 
 struct fsmonitor_settings;
fsmonitor.c +7 -0
index 57d6a483bee..540736b39fd 100644
@@ -295,6 +295,7 @@ static int fsmonitor_force_update_threshold = 100;
 
 void refresh_fsmonitor(struct index_state *istate)
 {
+	static int warn_once = 0;
 	struct strbuf query_result = STRBUF_INIT;
 	int query_success = 0, hook_version = -1;
 	size_t bol = 0; /* beginning of line */
@@ -305,6 +306,12 @@ void refresh_fsmonitor(struct index_state *istate)
 	int is_trivial = 0;
 	struct repository *r = istate->repo ? istate->repo : the_repository;
 	enum fsmonitor_mode fsm_mode = fsm_settings__get_mode(r);
+	enum fsmonitor_reason reason = fsm_settings__get_reason(r);
+
+	if (!warn_once && reason > FSMONITOR_REASON_OK) {
+		warn_once = 1;
+		warning("%s", fsm_settings__get_incompatible_msg(r, reason));
+	}
 
 	if (fsm_mode <= FSMONITOR_MODE_DISABLED ||
 	    istate->fsmonitor_has_run_once)
Signature · 1 line
gitgitgadget
#27 lore raw

PATCH v2 3/12 fsmonitor: avoid socket location check if using hook

In reply to Eric DeCosta via GitGitGadget

From: Eric DeCosta <[email protected]>

If monitoring is done via fsmonitor hook rather than IPC there is no need to check if the location of the Unix Domain socket (UDS) file is on a remote filesystem.

---
 compat/fsmonitor/fsm-settings-darwin.c | 10 ++++++----
 compat/fsmonitor/fsm-settings-win32.c  |  2 +-
 fsmonitor-settings.c                   |  8 ++++----
 fsmonitor-settings.h                   |  2 +-
 4 files changed, 12 insertions(+), 10 deletions(-)
compat/fsmonitor/fsm-settings-darwin.c +6 -4
index 7241c4c22c9..6abbc7af3ab 100644
@@ -48,13 +48,15 @@ static enum fsmonitor_reason check_uds_volume(struct repository *r)
 	return FSMONITOR_REASON_OK;
 }
 
-enum fsmonitor_reason fsm_os__incompatible(struct repository *r)
+enum fsmonitor_reason fsm_os__incompatible(struct repository *r, int ipc)
 {
 	enum fsmonitor_reason reason;
 
-	reason = check_uds_volume(r);
-	if (reason != FSMONITOR_REASON_OK)
-		return reason;
+	if (ipc) {
+		reason = check_uds_volume(r);
+		if (reason != FSMONITOR_REASON_OK)
+			return reason;
+	}
 
 	return FSMONITOR_REASON_OK;
 }
compat/fsmonitor/fsm-settings-win32.c +1 -1
index d88b06ae610..a8af31b71de 100644
@@ -25,7 +25,7 @@ static enum fsmonitor_reason check_vfs4git(struct repository *r)
 	return FSMONITOR_REASON_OK;
 }
 
-enum fsmonitor_reason fsm_os__incompatible(struct repository *r)
+enum fsmonitor_reason fsm_os__incompatible(struct repository *r, int ipc)
 {
 	enum fsmonitor_reason reason;
 
fsmonitor-settings.c +4 -4
index d288cbad479..531a1b6f956 100644
@@ -60,7 +60,7 @@ static enum fsmonitor_reason check_remote(struct repository *r)
 }
 #endif
 
-static enum fsmonitor_reason check_for_incompatible(struct repository *r)
+static enum fsmonitor_reason check_for_incompatible(struct repository *r, int ipc)
 {
 	if (!r->worktree) {
 		/*
@@ -77,7 +77,7 @@ static enum fsmonitor_reason check_for_incompatible(struct repository *r)
 		reason = check_remote(r);
 		if (reason != FSMONITOR_REASON_OK)
 			return reason;
-		reason = fsm_os__incompatible(r);
+		reason = fsm_os__incompatible(r, ipc);
 		if (reason != FSMONITOR_REASON_OK)
 			return reason;
 	}
@@ -162,7 +162,7 @@ const char *fsm_settings__get_hook_path(struct repository *r)
 
 void fsm_settings__set_ipc(struct repository *r)
 {
-	enum fsmonitor_reason reason = check_for_incompatible(r);
+	enum fsmonitor_reason reason = check_for_incompatible(r, 1);
 
 	if (reason != FSMONITOR_REASON_OK) {
 		fsm_settings__set_incompatible(r, reason);
@@ -185,7 +185,7 @@ void fsm_settings__set_ipc(struct repository *r)
 
 void fsm_settings__set_hook(struct repository *r, const char *path)
 {
-	enum fsmonitor_reason reason = check_for_incompatible(r);
+	enum fsmonitor_reason reason = check_for_incompatible(r, 0);
 
 	if (reason != FSMONITOR_REASON_OK) {
 		fsm_settings__set_incompatible(r, reason);
fsmonitor-settings.h +1 -1
index d9c2605197f..0721617b95a 100644
@@ -48,7 +48,7 @@ struct fsmonitor_settings;
  * fsm_os__* routines should considered private to fsm_settings__
  * routines.
  */
-enum fsmonitor_reason fsm_os__incompatible(struct repository *r);
+enum fsmonitor_reason fsm_os__incompatible(struct repository *r, int ipc);
 #endif /* HAVE_FSMONITOR_OS_SETTINGS */
 
 #endif /* FSMONITOR_SETTINGS_H */
Signature · 1 line
gitgitgadget
#28 lore raw

PATCH v2 4/12 fsmonitor: deal with synthetic firmlinks on macOS

In reply to Eric DeCosta via GitGitGadget

From: Eric DeCosta <[email protected]>

Starting with macOS 10.15 (Catalina), Apple introduced a new feature called 'firmlinks' in order to separate the boot volume into two volumes, one read-only and one writable but still present them to the user as a single volume. Along with this change, Apple removed the ability to create symlinks in the root directory and replaced them with 'synthetic firmlinks'. See 'man synthetic.conf'

When FSEevents reports the path of changed files, if the path involves a synthetic firmlink, the path is reported from the point of the synthetic firmlink and not the real path. For example:

Real path:
/System/Volumes/Data/network/working/directory/foo.txt

Synthetic firmlink:
/network -> /System/Volumes/Data/network

FSEvents path:
/network/working/directory/foo.txt

This causes the FSEvents path to not match against the worktree directory.

There are several ways in which synthetic firmlinks can be created:
they can be defined in /etc/synthetic.conf, the automounter can create them, and there may be other means. Simply reading /etc/synthetic.conf is insufficient. No matter what process creates synthetic firmlinks, they all get created in the root directory.

Therefore, in order to deal with synthetic firmlinks, the root directory is scanned and the first possible synthetic firmink that, when resolved, is a prefix of the worktree is used to map FSEvents paths to worktree paths.

---
 builtin/fsmonitor--daemon.c              |  8 +++
 compat/fsmonitor/fsm-listen-darwin.c     | 14 +++-
 compat/fsmonitor/fsm-path-utils-darwin.c | 92 ++++++++++++++++++++++++
 compat/fsmonitor/fsm-path-utils-win32.c  | 17 +++++
 fsmonitor--daemon.h                      |  3 +
 fsmonitor-path-utils.h                   | 36 +++++++++-
 6 files changed, 167 insertions(+), 3 deletions(-)
builtin/fsmonitor--daemon.c +8 -0
index 6e417846d8b..6f30a4f93a7 100644
@@ -3,6 +3,7 @@
 #include "parse-options.h"
 #include "fsmonitor.h"
 #include "fsmonitor-ipc.h"
+#include "fsmonitor-path-utils.h"
 #include "compat/fsmonitor/fsm-health.h"
 #include "compat/fsmonitor/fsm-listen.h"
 #include "fsmonitor--daemon.h"
@@ -1282,6 +1283,11 @@ static int fsmonitor_run_daemon(void)
 	strbuf_addstr(&state.path_worktree_watch, absolute_path(get_git_work_tree()));
 	state.nr_paths_watching = 1;
 
+	strbuf_init(&state.alias.alias, 0);
+	strbuf_init(&state.alias.points_to, 0);
+	if ((err = fsmonitor__get_alias(state.path_worktree_watch.buf, &state.alias)))
+		goto done;
+
 	/*
 	 * We create and delete cookie files somewhere inside the .git
 	 * directory to help us keep sync with the file system.  If
@@ -1391,6 +1397,8 @@ done:
 	strbuf_release(&state.path_gitdir_watch);
 	strbuf_release(&state.path_cookie_prefix);
 	strbuf_release(&state.path_ipc);
+	strbuf_release(&state.alias.alias);
+	strbuf_release(&state.alias.points_to);
 
 	return err;
 }
compat/fsmonitor/fsm-listen-darwin.c +12 -2
index 8e208e8289e..daeee4e465c 100644
@@ -26,6 +26,7 @@
 #include "fsmonitor.h"
 #include "fsm-listen.h"
 #include "fsmonitor--daemon.h"
+#include "fsmonitor-path-utils.h"
 
 struct fsm_listen_data
 {
@@ -198,8 +199,9 @@ static void fsevent_callback(ConstFSEventStreamRef streamRef,
 	struct string_list cookie_list = STRING_LIST_INIT_DUP;
 	const char *path_k;
 	const char *slash;
-	int k;
+	char *resolved = NULL;
 	struct strbuf tmp = STRBUF_INIT;
+	int k;
 
 	/*
 	 * Build a list of all filesystem changes into a private/local
@@ -209,7 +211,12 @@ static void fsevent_callback(ConstFSEventStreamRef streamRef,
 		/*
 		 * On Mac, we receive an array of absolute paths.
 		 */
-		path_k = paths[k];
+		free(resolved);
+		resolved = fsmonitor__resolve_alias(paths[k], &state->alias);
+		if (resolved)
+			path_k = resolved;
+		else
+			path_k = paths[k];
 
 		/*
 		 * If you want to debug FSEvents, log them to GIT_TRACE_FSMONITOR.
@@ -238,6 +245,7 @@ static void fsevent_callback(ConstFSEventStreamRef streamRef,
 			fsmonitor_force_resync(state);
 			fsmonitor_batch__free_list(batch);
 			string_list_clear(&cookie_list, 0);
+			batch = NULL;
 
 			/*
 			 * We assume that any events that we received
@@ -360,12 +368,14 @@ static void fsevent_callback(ConstFSEventStreamRef streamRef,
 		}
 	}
 
+	free(resolved);
 	fsmonitor_publish(state, batch, &cookie_list);
 	string_list_clear(&cookie_list, 0);
 	strbuf_release(&tmp);
 	return;
 
 force_shutdown:
+	free(resolved);
 	fsmonitor_batch__free_list(batch);
 	string_list_clear(&cookie_list, 0);
 
compat/fsmonitor/fsm-path-utils-darwin.c +92 -0

100 lines not on this page · raw message

compat/fsmonitor/fsm-path-utils-win32.c +17 -0
index a90b8f7925b..0d95bbb416f 100644
@@ -126,3 +126,20 @@ int fsmonitor__is_fs_remote(const char *path)
 		return -1;
 	return fs.is_remote;
 }
+
+/*
+ * No-op for now.
+ */
+int fsmonitor__get_alias(const char *path, struct alias_info *info)
+{
+	return 0;
+}
+
+/*
+ * No-op for now.
+ */
+char *fsmonitor__resolve_alias(const char *path,
+	const struct alias_info *info)
+{
+	return NULL;
+}
fsmonitor--daemon.h +3 -0
index 2102a5c9ff5..e24838f9a86 100644
@@ -8,6 +8,7 @@
 #include "run-command.h"
 #include "simple-ipc.h"
 #include "thread-utils.h"
+#include "fsmonitor-path-utils.h"
 
 struct fsmonitor_batch;
 struct fsmonitor_token_data;
@@ -43,6 +44,7 @@ struct fsmonitor_daemon_state {
 
 	struct strbuf path_worktree_watch;
 	struct strbuf path_gitdir_watch;
+	struct alias_info alias;
 	int nr_paths_watching;
 
 	struct fsmonitor_token_data *current_token_data;
@@ -59,6 +61,7 @@ struct fsmonitor_daemon_state {
 
 	struct ipc_server_data *ipc_server_data;
 	struct strbuf path_ipc;
+
 };
 
 /*
fsmonitor-path-utils.h +35 -1
index 41edf5b934f..5bfdfb81c14 100644
@@ -1,13 +1,21 @@
 #ifndef FSM_PATH_UTILS_H
 #define FSM_PATH_UTILS_H
 
+#include "strbuf.h"
+
+struct alias_info
+{
+	struct strbuf alias;
+	struct strbuf points_to;
+};
+
 struct fs_info {
 	int is_remote;
 	char *typename;
 };
 
 /*
- * Get some basic filesystem informtion for the given path
+ * Get some basic filesystem information for the given path
  *
  * The caller owns the storage that is occupied by fs_info and
  * is responsible for releasing it.
@@ -23,4 +31,30 @@ int fsmonitor__get_fs_info(const char *path, struct fs_info *fs_info);
  */
 int fsmonitor__is_fs_remote(const char *path);
 
+/*
+ * Get the alias in given path, if any.
+ *
+ * Sets alias to the first alias that matches any part of the path.
+ *
+ * If an alias is found, info.alias and info.points_to are set to the
+ * found mapping.
+ *
+ * Returns -1 on error, 0 otherwise.
+ *
+ * The caller owns the storage that is occupied by info.alias and
+ * info.points_to and is responsible for releasing it.
+ */
+int fsmonitor__get_alias(const char *path, struct alias_info *info);
+
+/*
+ * Resolve the path against the given alias.
+ *
+ * Returns the resolved path if there is one, NULL otherwise.
+ *
+ * The caller owns the storage that the returned string occupies and
+ * is responsible for releasing it.
+ */
+char *fsmonitor__resolve_alias(const char *path,
+	const struct alias_info *info);
+
 #endif
Signature · 1 line
gitgitgadget
#30 lore raw

PATCH v2 6/12 fsmonitor: add documentation for allowRemote and socketDir options

In reply to Eric DeCosta via GitGitGadget

From: Eric DeCosta <[email protected]>

Add documentation for 'fsmonitor.allowRemote' and 'fsmonitor.socketDir'. Call-out experimental nature of 'fsmonitor.allowRemote' and limited filesystem support for 'fsmonitor.socketDir'.

---
 Documentation/config.txt                   |  2 ++
 Documentation/config/fsmonitor--daemon.txt | 11 +++++++
 Documentation/git-fsmonitor--daemon.txt    | 37 ++++++++++++++++++++--
 3 files changed, 47 insertions(+), 3 deletions(-)
 create mode 100644 Documentation/config/fsmonitor--daemon.txt
Documentation/config.txt +2 -0
index 5b5b9765699..1e205831656 100644
@@ -423,6 +423,8 @@ include::config/filter.txt[]
 
 include::config/fsck.txt[]
 
+include::config/fsmonitor--daemon.txt[]
+
 include::config/gc.txt[]
 
 include::config/gitcvs.txt[]
Documentation/config/fsmonitor--daemon.txt new file +11 -0
new file mode 100644
index 00000000000..c225c6c9e74
@@ -0,0 +1,11 @@
+fsmonitor.allowRemote::
+    By default, the fsmonitor daemon refuses to work against network-mounted
+    repositories. Setting `fsmonitor.allowRemote` to `true` overrides this
+    behavior.  Only respected when `core.fsmonitor` is set to `true`.
+
+fsmonitor.socketDir::
+    This Mac OS-specific option, if set, specifies the directory in
+    which to create the Unix domain socket used for communication
+    between the fsmonitor daemon and various Git commands. The directory must
+    reside on a native Mac OS filesystem.  Only respected when `core.fsmonitor`
+    is set to `true`.
Documentation/git-fsmonitor--daemon.txt +34 -3
index cc142fb8612..8238eadb0e1 100644
@@ -3,7 +3,7 @@ git-fsmonitor{litdd}daemon(1)
 
 NAME
 ----
-git-fsmonitor--daemon - A Built-in File System Monitor
+git-fsmonitor--daemon - A Built-in Filesystem Monitor
 
 SYNOPSIS
 --------
@@ -17,7 +17,7 @@ DESCRIPTION
 -----------
 
 A daemon to watch the working directory for file and directory
-changes using platform-specific file system notification facilities.
+changes using platform-specific filesystem notification facilities.
 
 This daemon communicates directly with commands like `git status`
 using the link:technical/api-simple-ipc.html[simple IPC] interface
@@ -63,13 +63,44 @@ CAVEATS
 -------
 
 The fsmonitor daemon does not currently know about submodules and does
-not know to filter out file system events that happen within a
+not know to filter out filesystem 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
 will properly ignore these extra events, so performance may be affected
 but it will not cause an incorrect result.
 
+By default, the fsmonitor daemon refuses to work against network-mounted
+repositories; this may be overridden by setting `fsmonitor.allowRemote` to
+`true`. Note, however, that the fsmonitor daemon is not guaranteed to work
+correctly with all network-mounted repositories and such use is considered
+experimental.
+
+On Mac OS, the inter-process communication (IPC) between various Git
+commands and the fsmonitor daemon is done via a Unix domain socket (UDS) -- a
+special type of file -- which is supported by native Mac OS filesystems,
+but not on network-mounted filesystems, NTFS, or FAT32.  Other filesystems
+may or may not have the needed support; the fsmonitor daemon is not guaranteed
+to work with these filesystems and such use is considered experimental.
+
+By default, the socket is created in the `.git` directory, however, if the
+`.git` directory is on a network-mounted filesystem, it will be instead be
+created at `$HOME/.git-fsmonitor-*` unless `$HOME` itself is on a
+network-mounted filesystem in which case you must set the configuration
+variable `fsmonitor.socketDir` to the path of a directory on a Mac OS native
+filesystem in which to create the socket file.
+
+If none of the above directories (`.git`, `$HOME`, or `fsmonitor.socketDir`)
+is on a native Mac OS file filesystem the fsmonitor daemon will report an
+error that will cause the daemon and the currently running command to exit.
+
+CONFIGURATION
+-------------
+
+include::includes/cmd-config-section-all.txt[]
+
+include::config/fsmonitor--daemon.txt[]
+
 GIT
 ---
 Part of the linkgit:git[1] suite
Signature · 1 line
gitgitgadget
#31 lore raw

PATCH v2 7/12 fsmonitor: prepare to share code between Mac OS and Linux

In reply to Eric DeCosta via GitGitGadget

From: Eric DeCosta <[email protected]>

Linux and Mac OS can share some of the code originally developed for Mac OS.

Minor update to compat/fsmonitor/fsm-ipc-unix.c to make it cross-platform. Mac OS and Linux can share fsm-ipc-unix.c

Both platforms can also share compat/fsmonitor/fsm-settings-unix.c but we will leave room for future, platform-specific checks by having the platform- specific implementations call into fsm-settings-unix.

---
 Makefile                                      |  9 ++-
 compat/fsmonitor/fsm-health-linux.c           | 24 +++++++
 .../{fsm-ipc-darwin.c => fsm-ipc-unix.c}      |  8 +--
 compat/fsmonitor/fsm-settings-darwin.c        | 57 +----------------
 compat/fsmonitor/fsm-settings-linux.c         | 11 ++++
 compat/fsmonitor/fsm-settings-unix.c          | 62 +++++++++++++++++++
 compat/fsmonitor/fsm-settings-unix.h          | 11 ++++
 config.mak.uname                              |  1 +
 contrib/buildsystems/CMakeLists.txt           | 17 ++++-
 9 files changed, 138 insertions(+), 62 deletions(-)
 create mode 100644 compat/fsmonitor/fsm-health-linux.c
 rename compat/fsmonitor/{fsm-ipc-darwin.c => fsm-ipc-unix.c} (89%)
 create mode 100644 compat/fsmonitor/fsm-settings-linux.c
 create mode 100644 compat/fsmonitor/fsm-settings-unix.c
 create mode 100644 compat/fsmonitor/fsm-settings-unix.h
Makefile +8 -1
index 9553d590824..bbbde00ce49 100644
@@ -2036,13 +2036,20 @@ endif
 
 ifdef FSMONITOR_DAEMON_BACKEND
 	COMPAT_CFLAGS += -DHAVE_FSMONITOR_DAEMON_BACKEND
+	ifdef FSMONITOR_DAEMON_COMMON
+		COMPAT_OBJS += compat/fsmonitor/fsm-ipc-$(FSMONITOR_DAEMON_COMMON).o
+	else
+		COMPAT_OBJS += compat/fsmonitor/fsm-ipc-$(FSMONITOR_DAEMON_BACKEND).o
+	endif
 	COMPAT_OBJS += compat/fsmonitor/fsm-listen-$(FSMONITOR_DAEMON_BACKEND).o
 	COMPAT_OBJS += compat/fsmonitor/fsm-health-$(FSMONITOR_DAEMON_BACKEND).o
-	COMPAT_OBJS += compat/fsmonitor/fsm-ipc-$(FSMONITOR_DAEMON_BACKEND).o
 endif
 
 ifdef FSMONITOR_OS_SETTINGS
 	COMPAT_CFLAGS += -DHAVE_FSMONITOR_OS_SETTINGS
+	ifdef FSMONITOR_DAEMON_COMMON
+		COMPAT_OBJS += compat/fsmonitor/fsm-settings-$(FSMONITOR_DAEMON_COMMON).o
+	endif
 	COMPAT_OBJS += compat/fsmonitor/fsm-settings-$(FSMONITOR_OS_SETTINGS).o
 	COMPAT_OBJS += compat/fsmonitor/fsm-path-utils-$(FSMONITOR_OS_SETTINGS).o
 endif
compat/fsmonitor/fsm-health-linux.c new file +24 -0
new file mode 100644
index 00000000000..b9f709e8548
@@ -0,0 +1,24 @@
+#include "cache.h"
+#include "config.h"
+#include "fsmonitor.h"
+#include "fsm-health.h"
+#include "fsmonitor--daemon.h"
+
+int fsm_health__ctor(struct fsmonitor_daemon_state *state)
+{
+	return 0;
+}
+
+void fsm_health__dtor(struct fsmonitor_daemon_state *state)
+{
+	return;
+}
+
+void fsm_health__loop(struct fsmonitor_daemon_state *state)
+{
+	return;
+}
+
+void fsm_health__stop_async(struct fsmonitor_daemon_state *state)
+{
+}
compat/fsmonitor/fsm-ipc-unix.c renamed from compat/fsmonitor/fsm-ipc-darwin.c +4 -4
similarity index 89%
rename from compat/fsmonitor/fsm-ipc-darwin.c
rename to compat/fsmonitor/fsm-ipc-unix.c
index ce843d63348..3ba3b9e17ed 100644
@@ -10,7 +10,7 @@ static GIT_PATH_FUNC(fsmonitor_ipc__get_default_path, "fsmonitor--daemon.ipc")
 const char *fsmonitor_ipc__get_path(struct repository *r)
 {
 	static const char *ipc_path = NULL;
-	SHA_CTX sha1ctx;
+	git_SHA_CTX sha1ctx;
 	char *sock_dir = NULL;
 	struct strbuf ipc_file = STRBUF_INIT;
 	unsigned char hash[SHA_DIGEST_LENGTH];
@@ -28,9 +28,9 @@ const char *fsmonitor_ipc__get_path(struct repository *r)
 		return ipc_path;
 	}
 
-	SHA1_Init(&sha1ctx);
-	SHA1_Update(&sha1ctx, r->worktree, strlen(r->worktree));
-	SHA1_Final(hash, &sha1ctx);
+	git_SHA1_Init(&sha1ctx);
+	git_SHA1_Update(&sha1ctx, r->worktree, strlen(r->worktree));
+	git_SHA1_Final(hash, &sha1ctx);
 
 	repo_config_get_string(r, "fsmonitor.socketdir", &sock_dir);
 
compat/fsmonitor/fsm-settings-darwin.c +3 -54

65 lines not on this page · raw message

compat/fsmonitor/fsm-settings-linux.c new file +11 -0
new file mode 100644
index 00000000000..2f5c6a88bf0
@@ -0,0 +1,11 @@
+#include "config.h"
+#include "fsmonitor.h"
+#include "fsmonitor-ipc.h"
+#include "fsmonitor-path-utils.h"
+#include "fsmonitor-settings.h"
+#include "fsm-settings-unix.h"
+
+enum fsmonitor_reason fsm_os__incompatible(struct repository *r, int ipc)
+{
+    return fsm_os__incompatible_unix(r, ipc);
+}
compat/fsmonitor/fsm-settings-unix.c new file +62 -0

62 lines not on this page · raw message

compat/fsmonitor/fsm-settings-unix.h new file +11 -0
new file mode 100644
index 00000000000..8a62971190b
@@ -0,0 +1,11 @@
+#ifndef FSM_SETTINGS_UNIX_H
+#define FSM_SETTINGS_UNIX_H
+
+#ifdef HAVE_FSMONITOR_OS_SETTINGS
+/*
+ * Check for compatibility on unix-like systems (e.g. Darwin and Linux)
+ */
+enum fsmonitor_reason fsm_os__incompatible_unix(struct repository *r, int ipc);
+#endif /* HAVE_FSMONITOR_OS_SETTINGS */
+
+#endif /* FSM_SETTINGS_UNIX_H */
config.mak.uname +1 -0
index d63629fe807..9f716cfba81 100644
@@ -165,6 +165,7 @@ ifeq ($(uname_S),Darwin)
 	ifndef NO_UNIX_SOCKETS
 	FSMONITOR_DAEMON_BACKEND = darwin
 	FSMONITOR_OS_SETTINGS = darwin
+	FSMONITOR_DAEMON_COMMON = unix
 	endif
 	endif
 
contrib/buildsystems/CMakeLists.txt +14 -3
index 787738e6fa3..4c6e84da346 100644
@@ -304,7 +304,17 @@ else()
 endif()
 
 if(SUPPORTS_SIMPLE_IPC)
-	if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
+	if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
+		add_compile_definitions(HAVE_FSMONITOR_DAEMON_BACKEND)
+		list(APPEND compat_SOURCES compat/fsmonitor/fsm-ipc-unix.c)
+		list(APPEND compat_SOURCES compat/fsmonitor/fsm-health-linux.c)
+		list(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-linux.c)
+		list(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-linux.c)
+
+		add_compile_definitions(HAVE_FSMONITOR_OS_SETTINGS)
+		list(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-unix.c)
+		list(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-linux.c)
+	elseif(CMAKE_SYSTEM_NAME STREQUAL "Windows")
 		add_compile_definitions(HAVE_FSMONITOR_DAEMON_BACKEND)
 		list(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-win32.c)
 		list(APPEND compat_SOURCES compat/fsmonitor/fsm-health-win32.c)
@@ -315,12 +325,13 @@ if(SUPPORTS_SIMPLE_IPC)
 		list(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-win32.c)
 	elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
 		add_compile_definitions(HAVE_FSMONITOR_DAEMON_BACKEND)
-		list(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-darwin.c)
+		list(APPEND compat_SOURCES compat/fsmonitor/fsm-ipc-unix.c)
 		list(APPEND compat_SOURCES compat/fsmonitor/fsm-health-darwin.c)
-		list(APPEND compat_SOURCES compat/fsmonitor/fsm-ipc-darwin.c)
+		list(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-darwin.c)
 		list(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-darwin.c)
 
 		add_compile_definitions(HAVE_FSMONITOR_OS_SETTINGS)
+		list(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-unix.c)
 		list(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-darwin.c)
 	endif()
 endif()
Signature · 1 line
gitgitgadget
#38 lore raw

PATCH v2 10/12 fsmonitor: enable fsmonitor for Linux

In reply to Eric DeCosta via GitGitGadget

From: Eric DeCosta <[email protected]>

Update build to enable fsmonitor for Linux.

---
 config.mak.uname | 9 +++++++++
 1 file changed, 9 insertions(+)
config.mak.uname +9 -0
index 9f716cfba81..d8889f8570a 100644
@@ -68,6 +68,15 @@ ifeq ($(uname_S),Linux)
 	ifneq ($(findstring .el7.,$(uname_R)),)
 		BASIC_CFLAGS += -std=c99
 	endif
+	# The builtin FSMonitor on Linux builds upon Simple-IPC.  Both require
+	# Unix domain sockets and PThreads.
+	ifndef NO_PTHREADS
+	ifndef NO_UNIX_SOCKETS
+	FSMONITOR_DAEMON_BACKEND = linux
+	FSMONITOR_OS_SETTINGS = linux
+	FSMONITOR_DAEMON_COMMON = unix
+	endif
+	endif
 endif
 ifeq ($(uname_S),GNU/kFreeBSD)
 	HAVE_ALLOCA_H = YesPlease
Signature · 1 line
gitgitgadget
#39 lore raw

PATCH v2 11/12 fsmonitor: test updates

In reply to Eric DeCosta via GitGitGadget

From: Eric DeCosta <[email protected]>

t7527-builtin-fsmonitor was leaking fsmonitor--daemon processes in some cases.

Accomodate slight difference in the number of events generated on Linux.

On lower-powered systems, spin a little to give the daemon time to respond to and log filesystem events.

---
 t/t7527-builtin-fsmonitor.sh | 72 +++++++++++++++++++++++++++++-------
 1 file changed, 58 insertions(+), 14 deletions(-)
t/t7527-builtin-fsmonitor.sh +58 -14
index 1746d30cf6a..d27bd3662a7 100755
@@ -13,7 +13,7 @@ fi
 stop_daemon_delete_repo () {
 	r=$1 &&
 	test_might_fail git -C $r fsmonitor--daemon stop &&
-	rm -rf $1
+	rm -rf $r
 }
 
 start_daemon () {
@@ -72,6 +72,32 @@ start_daemon () {
 	)
 }
 
+IMPLICIT_TIMEOUT=5
+
+wait_for_update () {
+	func=$1 &&
+	file=$2 &&
+	sz=$(wc -c < "$file") &&
+	last=0 &&
+	$func &&
+	k=0 &&
+	while test "$k" -lt $IMPLICIT_TIMEOUT
+	do
+		nsz=$(wc -c < "$file")
+		if test "$nsz" -gt "$sz"
+		then
+			if test "$last" -eq "$nsz"
+			then
+				return 0
+			fi
+			last=$nsz
+		fi
+		sleep 1
+		k=$(( $k + 1 ))
+	done &&
+	return 0
+}
+
 # Is a Trace2 data event present with the given catetory and key?
 # We do not care what the value is.
 #
@@ -137,7 +163,6 @@ test_expect_success 'implicit daemon start' '
 # machines (where it might take a moment to wake and reschedule the
 # daemon process) to avoid false alarms during test runs.)
 #
-IMPLICIT_TIMEOUT=5
 
 verify_implicit_shutdown () {
 	r=$1 &&
@@ -373,6 +398,10 @@ create_files () {
 	echo 3 >dir2/new
 }
 
+rename_directory () {
+	mv dirtorename dirrenamed
+}
+
 rename_files () {
 	mv rename renamed &&
 	mv dir1/rename dir1/renamed &&
@@ -427,10 +456,12 @@ test_expect_success 'edit some files' '
 
 	start_daemon --tf "$PWD/.git/trace" &&
 
-	edit_files &&
+	wait_for_update edit_files "$PWD/.git/trace" &&
 
 	test-tool fsmonitor-client query --token 0 &&
 
+	test_might_fail git fsmonitor--daemon stop &&
+
 	grep "^event: dir1/modified$"  .git/trace &&
 	grep "^event: dir2/modified$"  .git/trace &&
 	grep "^event: modified$"       .git/trace &&
@@ -442,10 +473,12 @@ test_expect_success 'create some files' '
 
 	start_daemon --tf "$PWD/.git/trace" &&
 
-	create_files &&
+	wait_for_update create_files "$PWD/.git/trace" &&
 
 	test-tool fsmonitor-client query --token 0 &&
 
+	test_might_fail git fsmonitor--daemon stop &&
+
 	grep "^event: dir1/new$" .git/trace &&
 	grep "^event: dir2/new$" .git/trace &&
 	grep "^event: new$"      .git/trace
@@ -456,10 +489,12 @@ test_expect_success 'delete some files' '
 
 	start_daemon --tf "$PWD/.git/trace" &&
 
-	delete_files &&
+	wait_for_update delete_files "$PWD/.git/trace" &&
 
 	test-tool fsmonitor-client query --token 0 &&
 
+	test_might_fail git fsmonitor--daemon stop &&
+
 	grep "^event: dir1/delete$" .git/trace &&
 	grep "^event: dir2/delete$" .git/trace &&
 	grep "^event: delete$"      .git/trace
@@ -470,10 +505,12 @@ test_expect_success 'rename some files' '
 
 	start_daemon --tf "$PWD/.git/trace" &&
 
-	rename_files &&
+	wait_for_update rename_files "$PWD/.git/trace" &&
 
 	test-tool fsmonitor-client query --token 0 &&
 
+	test_might_fail git fsmonitor--daemon stop &&
+
 	grep "^event: dir1/rename$"  .git/trace &&
 	grep "^event: dir2/rename$"  .git/trace &&
 	grep "^event: rename$"       .git/trace &&
@@ -487,10 +524,12 @@ test_expect_success 'rename directory' '
 
 	start_daemon --tf "$PWD/.git/trace" &&
 
-	mv dirtorename dirrenamed &&
+	wait_for_update rename_directory "$PWD/.git/trace" &&
 
 	test-tool fsmonitor-client query --token 0 &&
 
+	test_might_fail git fsmonitor--daemon stop &&
+
 	grep "^event: dirtorename/*$" .git/trace &&
 	grep "^event: dirrenamed/*$"  .git/trace
 '
@@ -500,10 +539,12 @@ test_expect_success 'file changes to directory' '
 
 	start_daemon --tf "$PWD/.git/trace" &&
 
-	file_to_directory &&
+	wait_for_update file_to_directory "$PWD/.git/trace" &&
 
 	test-tool fsmonitor-client query --token 0 &&
 
+	test_might_fail git fsmonitor--daemon stop &&
+
 	grep "^event: delete$"     .git/trace &&
 	grep "^event: delete/new$" .git/trace
 '
@@ -513,10 +554,12 @@ test_expect_success 'directory changes to a file' '
 
 	start_daemon --tf "$PWD/.git/trace" &&
 
-	directory_to_file &&
+	wait_for_update directory_to_file "$PWD/.git/trace" &&
 
 	test-tool fsmonitor-client query --token 0 &&
 
+	test_might_fail git fsmonitor--daemon stop &&
+
 	grep "^event: dir1$" .git/trace
 '
 
@@ -561,7 +604,7 @@ test_expect_success 'flush cached data' '
 	test-tool -C test_flush fsmonitor-client query --token "builtin:test_00000002:0" >actual_2 &&
 	nul_to_q <actual_2 >actual_q2 &&
 
-	grep "^builtin:test_00000002:0Q$" actual_q2 &&
+	grep "^builtin:test_00000002:[0-1]Q$" actual_q2 &&
 
 	>test_flush/file_3 &&
 
@@ -732,7 +775,8 @@ u_values="$u1 $u2"
 for u in $u_values
 do
 	test_expect_success "unicode in repo root path: $u" '
-		test_when_finished "stop_daemon_delete_repo $u" &&
+		test_when_finished \
+		"stop_daemon_delete_repo `echo "$u" | sed 's:x:\\\\\\\\\\\\\\x:g'`" &&
 
 		git init "$u" &&
 		echo 1 >"$u"/file1 &&
@@ -814,8 +858,7 @@ my_match_and_clean () {
 }
 
 test_expect_success 'submodule always visited' '
-	test_when_finished "git -C super fsmonitor--daemon stop; \
-			    rm -rf super; \
+	test_when_finished "rm -rf super; \
 			    rm -rf sub" &&
 
 	create_super super &&
@@ -883,7 +926,8 @@ have_t2_error_event () {
 }
 
 test_expect_success "stray submodule super-prefix warning" '
-	test_when_finished "rm -rf super; \
+	test_when_finished "git -C super/dir_1/dir_2/sub fsmonitor--daemon stop; \
+			    rm -rf super; \
 			    rm -rf sub;   \
 			    rm super-sub.trace" &&
 
Signature · 1 line
gitgitgadget
#36 lore raw

PATCH v2 9/12 fsmonitor: implement filesystem change listener for Linux

In reply to Eric DeCosta via GitGitGadget

From: Eric DeCosta <[email protected]>

Implement a filesystem change listener for Linux based on the inotify API:
https://man7.org/linux/man-pages/man7/inotify.7.html

inotify requires registering a watch on every directory in the worktree and special handling of moves/renames.

---
 compat/fsmonitor/fsm-listen-linux.c | 664 ++++++++++++++++++++++++++++
 1 file changed, 664 insertions(+)
 create mode 100644 compat/fsmonitor/fsm-listen-linux.c
compat/fsmonitor/fsm-listen-linux.c new file +664 -0
new file mode 100644
index 00000000000..0ac131a9497
@@ -0,0 +1,664 @@
+#include "cache.h"
+#include "fsmonitor.h"
+#include "fsm-listen.h"
+#include "fsmonitor--daemon.h"
+#include <dirent.h>
+#include <fcntl.h>
+#include <sys/inotify.h>
+#include <sys/stat.h>
+
+/*
+ * Safe value to bitwise OR with rest of mask for
+ * kernels that do not support IN_MASK_CREATE
+ */
+#ifndef IN_MASK_CREATE
+#define IN_MASK_CREATE 0x00000000
+#endif
+
+enum shutdown_reason {
+	SHUTDOWN_CONTINUE = 0,
+	SHUTDOWN_STOP,
+	SHUTDOWN_ERROR,
+	SHUTDOWN_FORCE
+};
+
+struct watch_entry {
+	struct hashmap_entry ent;
+	int wd;
+	uint32_t cookie;
+	const char *dir;
+};
+
+struct rename_entry {
+	struct hashmap_entry ent;
+	time_t whence;
+	uint32_t cookie;
+	const char *dir;
+};
+
+struct fsm_listen_data {
+	int fd_inotify;
+	enum shutdown_reason shutdown;
+	struct hashmap watches;
+	struct hashmap renames;
+	struct hashmap revwatches;
+};
+
+static int watch_entry_cmp(const void *cmp_data,
+			  const struct hashmap_entry *eptr,
+			  const struct hashmap_entry *entry_or_key,
+			  const void *keydata)
+{
+	const struct watch_entry *e1, *e2;
+
+	e1 = container_of(eptr, const struct watch_entry, ent);
+	e2 = container_of(eptr, const struct watch_entry, ent);
+	return e1->wd != e2->wd;
+}
+
+static int revwatches_entry_cmp(const void *cmp_data,
+			  const struct hashmap_entry *eptr,
+			  const struct hashmap_entry *entry_or_key,
+			  const void *keydata)
+{
+	const struct watch_entry *e1, *e2;
+
+	e1 = container_of(eptr, const struct watch_entry, ent);
+	e2 = container_of(eptr, const struct watch_entry, ent);
+	return strcmp(e1->dir, e2->dir);
+}
+
+static int rename_entry_cmp(const void *cmp_data,
+			  const struct hashmap_entry *eptr,
+			  const struct hashmap_entry *entry_or_key,
+			  const void *keydata)
+{
+	const struct rename_entry *e1, *e2;
+
+	e1 = container_of(eptr, const struct rename_entry, ent);
+	e2 = container_of(eptr, const struct rename_entry, ent);
+	return e1->cookie != e2->cookie;
+}
+
+/*
+ * Register an inotify watch, add watch descriptor to path mapping
+ * and the reverse mapping.
+ */
+static int add_watch(const char *path, struct fsm_listen_data *data)
+{
+	const char *interned = strintern(path);
+	struct watch_entry *w1, *w2;
+
+	/* add the inotify watch, don't allow watches to be modified */
+	int wd = inotify_add_watch(data->fd_inotify, interned,
+				(IN_ALL_EVENTS | IN_ONLYDIR | IN_MASK_CREATE)
+				^ IN_ACCESS ^ IN_CLOSE ^ IN_OPEN);
+	if (wd < 0)
+		return error_errno("inotify_add_watch('%s') failed", interned);
+
+	/* add watch descriptor -> directory mapping */
+	CALLOC_ARRAY(w1, 1);
+	w1->wd = wd;
+	w1->dir = interned;
+	hashmap_entry_init(&w1->ent, memhash(&w1->wd, sizeof(int)));
+	hashmap_add(&data->watches, &w1->ent);
+
+	/* add directory -> watch descriptor mapping */
+	CALLOC_ARRAY(w2, 1);
+	w2->wd = wd;
+	w2->dir = interned;
+	hashmap_entry_init(&w2->ent, memhash(w2->dir, strlen(w2->dir)));
+	hashmap_add(&data->revwatches, &w2->ent);
+
+	return 0;
+}
+
+/*
+ * Remove the inotify watch, the watch descriptor to path mapping
+ * and the reverse mapping.
+ */
+static void remove_watch(struct watch_entry *w,
+	struct fsm_listen_data *data)
+{
+	struct watch_entry k1, k2, *w1, *w2;
+
+	/* remove watch, ignore error if kernel already did it */
+	if (inotify_rm_watch(data->fd_inotify, w->wd) && errno != EINVAL)
+		error_errno("inotify_rm_watch() failed");
+
+	hashmap_entry_init(&k1.ent, memhash(&w->wd, sizeof(int)));
+	w1 = hashmap_remove_entry(&data->watches, &k1, ent, NULL);
+	if (!w1)
+		BUG("Double remove of watch for '%s'", w->dir);
+
+	if (w1->cookie)
+		BUG("Removing watch for '%s' which has a pending rename", w1->dir);
+
+	hashmap_entry_init(&k2.ent, memhash(w->dir, strlen(w->dir)));
+	w2 = hashmap_remove_entry(&data->revwatches, &k2, ent, NULL);
+	if (!w2)
+		BUG("Double remove of reverse watch for '%s'", w->dir);
+
+	/* w1->dir and w2->dir are interned strings, we don't own them */
+	free(w1);
+	free(w2);
+}
+
+/*
+ * Check for stale directory renames.
+ *
+ * https://man7.org/linux/man-pages/man7/inotify.7.html
+ *
+ * Allow for some small timeout to account for the fact that insertion of the
+ * IN_MOVED_FROM+IN_MOVED_TO event pair is not atomic, and the possibility that
+ * there may not be any IN_MOVED_TO event.
+ *
+ * If the IN_MOVED_TO event is not received within the timeout then events have
+ * been missed and the monitor is in an inconsistent state with respect to the
+ * filesystem.
+ */
+static int check_stale_dir_renames(struct hashmap *renames, time_t max_age)
+{
+	struct rename_entry *re;
+	struct hashmap_iter iter;
+
+	hashmap_for_each_entry(renames, &iter, re, ent) {
+		if (re->whence <= max_age)
+			return -1;
+	}
+	return 0;
+}
+
+/*
+ * Track pending renames.
+ *
+ * Tracking is done via a event cookie to watch descriptor mapping.
+ *
+ * A rename is not complete until matching a IN_MOVED_TO event is received
+ * for a corresponding IN_MOVED_FROM event.
+ */
+static void add_dir_rename(uint32_t cookie, const char *path,
+	struct fsm_listen_data *data)
+{
+	struct watch_entry k, *w;
+	struct rename_entry *re;
+
+	/* lookup the watch descriptor for the given path */
+	hashmap_entry_init(&k.ent, memhash(path, strlen(path)));
+	w = hashmap_get_entry(&data->revwatches, &k, ent, NULL);
+	if (!w) /* should never happen */
+		BUG("No watch for '%s'", path);
+	w->cookie = cookie;
+
+	/* add the pending rename to match against later */
+	CALLOC_ARRAY(re, 1);
+	re->dir = w->dir;
+	re->cookie = w->cookie;
+	re->whence = time(NULL);
+	hashmap_entry_init(&re->ent, memhash(&re->cookie, sizeof(uint32_t)));
+	hashmap_add(&data->renames, &re->ent);
+}
+
+/*
+ * Handle directory renames
+ *
+ * Once a IN_MOVED_TO event is received, lookup the rename tracking information
+ * via the event cookie and use this information to update the watch.
+ */
+static void rename_dir(uint32_t cookie, const char *path,
+	struct fsm_listen_data *data)
+{
+	struct rename_entry rek, *re;
+	struct watch_entry k, *w;
+
+	/* lookup a pending rename to match */
+	rek.cookie = cookie;
+	hashmap_entry_init(&rek.ent, memhash(&rek.cookie, sizeof(uint32_t)));
+	re = hashmap_get_entry(&data->renames, &rek, ent, NULL);
+	if (re) {
+		k.dir = re->dir;
+		hashmap_entry_init(&k.ent, memhash(k.dir, strlen(k.dir)));
+		w = hashmap_get_entry(&data->revwatches, &k, ent, NULL);
+		if (w) {
+			w->cookie = 0; /* rename handled */
+			remove_watch(w, data);
+			add_watch(path, data);
+		} else {
+			BUG("No matching watch");
+		}
+	} else {
+		BUG("No matching cookie");
+	}
+}
+
+/*
+ * Recursively add watches to every directory under path
+ */
+static int register_inotify(const char *path, struct fsm_listen_data *data)
+{
+	DIR *dir;
+	struct strbuf current = STRBUF_INIT;
+	struct dirent *de;
+	struct stat fs;
+	int ret = -1;
+
+	dir = opendir(path);
+	if (!dir)
+		return error_errno("opendir('%s') failed", path);
+
+	while ((de = readdir_skip_dot_and_dotdot(dir)) != NULL) {
+		strbuf_reset(&current);
+		strbuf_addf(&current, "%s/%s", path, de->d_name);
+		if (lstat(current.buf, &fs)) {
+			error_errno("lstat('%s') failed", current.buf);
+			goto failed;
+		}
+
+		/* recurse into directory */
+		if (S_ISDIR(fs.st_mode)) {
+			if (add_watch(current.buf, data))
+				goto failed;
+			if (register_inotify(current.buf, data))
+				goto failed;
+		}
+	}
+	ret = 0;
+
+failed:
+	strbuf_release(&current);
+	if (closedir(dir) < 0)
+		return error_errno("closedir('%s') failed", path);
+	return ret;
+}
+
+static int em_rename_dir_from(u_int32_t mask)
+{
+	return ((mask & IN_ISDIR) && (mask & IN_MOVED_FROM));
+}
+
+static int em_rename_dir_to(u_int32_t mask)
+{
+	return ((mask & IN_ISDIR) && (mask & IN_MOVED_TO));
+}
+
+static int em_remove_watch(u_int32_t mask)
+{
+	return (mask & IN_DELETE_SELF);
+}
+
+static int em_dir_renamed(u_int32_t mask)
+{
+	return ((mask & IN_ISDIR) && (mask & IN_MOVE));
+}
+
+static int em_dir_created(u_int32_t mask)
+{
+	return ((mask & IN_ISDIR) && (mask & IN_CREATE));
+}
+
+static int em_dir_deleted(uint32_t mask)
+{
+	return ((mask & IN_ISDIR) && (mask & IN_DELETE));
+}
+
+static int em_force_shutdown(u_int32_t mask)
+{
+	return (mask & IN_UNMOUNT) || (mask & IN_Q_OVERFLOW);
+}
+
+static int em_ignore(u_int32_t mask)
+{
+	return (mask & IN_IGNORED) || (mask & IN_MOVE_SELF);
+}
+
+static void log_mask_set(const char *path, u_int32_t mask)
+{
+	struct strbuf msg = STRBUF_INIT;
+
+	if (mask & IN_ACCESS)
+		strbuf_addstr(&msg, "IN_ACCESS|");
+	if (mask & IN_MODIFY)
+		strbuf_addstr(&msg, "IN_MODIFY|");
+	if (mask & IN_ATTRIB)
+		strbuf_addstr(&msg, "IN_ATTRIB|");
+	if (mask & IN_CLOSE_WRITE)
+		strbuf_addstr(&msg, "IN_CLOSE_WRITE|");
+	if (mask & IN_CLOSE_NOWRITE)
+		strbuf_addstr(&msg, "IN_CLOSE_NOWRITE|");
+	if (mask & IN_OPEN)
+		strbuf_addstr(&msg, "IN_OPEN|");
+	if (mask & IN_MOVED_FROM)
+		strbuf_addstr(&msg, "IN_MOVED_FROM|");
+	if (mask & IN_MOVED_TO)
+		strbuf_addstr(&msg, "IN_MOVED_TO|");
+	if (mask & IN_CREATE)
+		strbuf_addstr(&msg, "IN_CREATE|");
+	if (mask & IN_DELETE)
+		strbuf_addstr(&msg, "IN_DELETE|");
+	if (mask & IN_DELETE_SELF)
+		strbuf_addstr(&msg, "IN_DELETE_SELF|");
+	if (mask & IN_MOVE_SELF)
+		strbuf_addstr(&msg, "IN_MOVE_SELF|");
+	if (mask & IN_UNMOUNT)
+		strbuf_addstr(&msg, "IN_UNMOUNT|");
+	if (mask & IN_Q_OVERFLOW)
+		strbuf_addstr(&msg, "IN_Q_OVERFLOW|");
+	if (mask & IN_IGNORED)
+		strbuf_addstr(&msg, "IN_IGNORED|");
+	if (mask & IN_ISDIR)
+		strbuf_addstr(&msg, "IN_ISDIR|");
+
+	trace_printf_key(&trace_fsmonitor, "inotify_event: '%s', mask=%#8.8x %s",
+				path, mask, msg.buf);
+
+	strbuf_release(&msg);
+}
+
+int fsm_listen__ctor(struct fsmonitor_daemon_state *state)
+{
+	int fd;
+	int ret = 0;
+	struct fsm_listen_data *data;
+
+	CALLOC_ARRAY(data, 1);
+	state->listen_data = data;
+	state->listen_error_code = -1;
+	data->shutdown = SHUTDOWN_ERROR;
+
+	fd = inotify_init1(O_NONBLOCK);
+	if (fd < 0)
+		return error_errno("inotify_init1() failed");
+
+	data->fd_inotify = fd;
+
+	hashmap_init(&data->watches, watch_entry_cmp, NULL, 0);
+	hashmap_init(&data->renames, rename_entry_cmp, NULL, 0);
+	hashmap_init(&data->revwatches, revwatches_entry_cmp, NULL, 0);
+
+	if (add_watch(state->path_worktree_watch.buf, data))
+		ret = -1;
+	else if (register_inotify(state->path_worktree_watch.buf, data))
+		ret = -1;
+	else if (state->nr_paths_watching > 1) {
+		if (add_watch(state->path_gitdir_watch.buf, data))
+			ret = -1;
+		else if (register_inotify(state->path_gitdir_watch.buf, data))
+			ret = -1;
+	}
+
+	if (!ret) {
+		state->listen_error_code = 0;
+		data->shutdown = SHUTDOWN_CONTINUE;
+	}
+
+	return ret;
+}
+
+void fsm_listen__dtor(struct fsmonitor_daemon_state *state)
+{
+	struct fsm_listen_data *data;
+	struct hashmap_iter iter;
+	struct watch_entry *w;
+	int fd;
+
+	if (!state || !state->listen_data)
+		return;
+
+	data = state->listen_data;
+	fd = data->fd_inotify;
+
+	hashmap_for_each_entry(&data->watches, &iter, w, ent) {
+		w->cookie = 0; /* ignore any pending renames */
+		remove_watch(w, data);
+	}
+	hashmap_clear(&data->watches);
+
+	hashmap_clear(&data->revwatches); /* remove_watch freed the entries */
+
+	hashmap_clear_and_free(&data->renames, struct rename_entry, ent);
+
+	FREE_AND_NULL(state->listen_data);
+
+	if (fd && (close(fd) < 0))
+		error_errno(_("closing inotify file descriptor failed"));
+}
+
+void fsm_listen__stop_async(struct fsmonitor_daemon_state *state)
+{
+	if (!state->listen_data->shutdown)
+		state->listen_data->shutdown = SHUTDOWN_STOP;
+}
+
+/*
+ * Process a single inotify event and queue for publication.
+ */
+static int process_event(const char *path,
+	const struct inotify_event *event,
+	struct fsmonitor_batch *batch,
+	struct string_list *cookie_list,
+	struct fsmonitor_daemon_state *state)
+{
+	const char *rel;
+	const char *last_sep;
+
+	switch (fsmonitor_classify_path_absolute(state, path)) {
+		case IS_INSIDE_DOT_GIT_WITH_COOKIE_PREFIX:
+		case IS_INSIDE_GITDIR_WITH_COOKIE_PREFIX:
+			/* Use just the filename of the cookie file. */
+			last_sep = find_last_dir_sep(path);
+			string_list_append(cookie_list,
+					last_sep ? last_sep + 1 : path);
+			break;
+		case IS_INSIDE_DOT_GIT:
+		case IS_INSIDE_GITDIR:
+			break;
+		case IS_DOT_GIT:
+		case IS_GITDIR:
+			/*
+			* If .git directory is deleted or renamed away,
+			* we have to quit.
+			*/
+			if (em_dir_deleted(event->mask)) {
+				trace_printf_key(&trace_fsmonitor,
+						"event: gitdir removed");
+				state->listen_data->shutdown = SHUTDOWN_FORCE;
+				goto done;
+			}
+
+			if (em_dir_renamed(event->mask)) {
+				trace_printf_key(&trace_fsmonitor,
+						"event: gitdir renamed");
+				state->listen_data->shutdown = SHUTDOWN_FORCE;
+				goto done;
+			}
+			break;
+		case IS_WORKDIR_PATH:
+			/* normal events in the working directory */
+			if (trace_pass_fl(&trace_fsmonitor))
+				log_mask_set(path, event->mask);
+
+			rel = path + state->path_worktree_watch.len + 1;
+			fsmonitor_batch__add_path(batch, rel);
+
+			if (em_dir_deleted(event->mask))
+				break;
+
+			/* received IN_MOVE_FROM, add tracking for expected IN_MOVE_TO */
+			if (em_rename_dir_from(event->mask))
+				add_dir_rename(event->cookie, path, state->listen_data);
+
+			/* received IN_MOVE_TO, update watch to reflect new path */
+			if (em_rename_dir_to(event->mask))
+				rename_dir(event->cookie, path, state->listen_data);
+
+			if (em_dir_created(event->mask)) {
+				if (add_watch(path, state->listen_data)) {
+					state->listen_data->shutdown = SHUTDOWN_ERROR;
+					goto done;
+				}
+				if (register_inotify(path, state->listen_data)) {
+					state->listen_data->shutdown = SHUTDOWN_ERROR;
+					goto done;
+				}
+			}
+			break;
+		case IS_OUTSIDE_CONE:
+		default:
+			trace_printf_key(&trace_fsmonitor,
+					"ignoring '%s'", path);
+			break;
+	}
+	return 0;
+done:
+	return -1;
+}
+
+/*
+ * Read the inotify event stream and pre-process events before further
+ * processing and eventual publishing.
+ */
+static void handle_events(struct fsmonitor_daemon_state *state)
+{
+	 /* See https://man7.org/linux/man-pages/man7/inotify.7.html */
+	char buf[4096]
+		__attribute__ ((aligned(__alignof__(struct inotify_event))));
+
+	struct hashmap watches = state->listen_data->watches;
+	struct fsmonitor_batch *batch = NULL;
+	struct string_list cookie_list = STRING_LIST_INIT_DUP;
+	struct watch_entry k, *w;
+	struct strbuf path;
+	const struct inotify_event *event;
+	int fd = state->listen_data->fd_inotify;
+	ssize_t len;
+	char *ptr, *p;
+
+	strbuf_init(&path, PATH_MAX);
+
+	for(;;) {
+		len = read(fd, buf, sizeof(buf));
+		if (len == -1 && errno != EAGAIN) {
+			error_errno(_("reading inotify message stream failed"));
+			state->listen_data->shutdown = SHUTDOWN_ERROR;
+			goto done;
+		}
+
+		/* nothing to read */
+		if (len <= 0)
+			goto done;
+
+		/* Loop over all events in the buffer. */
+		for (ptr = buf; ptr < buf + len;
+			 ptr += sizeof(struct inotify_event) + event->len) {
+
+			event = (const struct inotify_event *) ptr;
+
+			if (em_ignore(event->mask))
+				continue;
+
+			/* File system was unmounted or event queue overflowed */
+			if (em_force_shutdown(event->mask)) {
+				if (trace_pass_fl(&trace_fsmonitor))
+					log_mask_set("Forcing shutdown", event->mask);
+				state->listen_data->shutdown = SHUTDOWN_FORCE;
+				goto done;
+			}
+
+			hashmap_entry_init(&k.ent, memhash(&event->wd, sizeof(int)));
+			k.wd = event->wd;
+
+			w = hashmap_get_entry(&watches, &k, ent, NULL);
+			if (!w) /* should never happen */
+				BUG("No watch for '%s'", event->name);
+
+			/* directory watch was removed */
+			if (em_remove_watch(event->mask)) {
+				remove_watch(w, state->listen_data);
+				continue;
+			}
+
+			strbuf_reset(&path);
+			strbuf_add(&path, w->dir, strlen(w->dir));
+			strbuf_addch(&path, '/');
+			strbuf_add(&path, event->name,  strlen(event->name));
+
+			p = fsmonitor__resolve_alias(path.buf, &state->alias);
+			if (!p)
+				p = strbuf_detach(&path, NULL);
+
+			if (!batch)
+				batch = fsmonitor_batch__new();
+
+			if (process_event(p, event, batch, &cookie_list, state)) {
+				free(p);
+				goto done;
+			}
+			free(p);
+		}
+		strbuf_reset(&path);
+		fsmonitor_publish(state, batch, &cookie_list);
+		string_list_clear(&cookie_list, 0);
+		batch = NULL;
+	}
+done:
+	strbuf_release(&path);
+	fsmonitor_batch__free_list(batch);
+	string_list_clear(&cookie_list, 0);
+}
+
+/*
+ * Non-blocking read of the inotify events stream. The inotify fd is polled
+ * frequently to help minimize the number of queue overflows.
+ */
+void fsm_listen__loop(struct fsmonitor_daemon_state *state)
+{
+	int poll_num;
+	const int interval = 1000;
+	time_t checked = time(NULL);
+	struct pollfd fds[1];
+	fds[0].fd = state->listen_data->fd_inotify;
+	fds[0].events = POLLIN;
+
+	for(;;) {
+		switch (state->listen_data->shutdown) {
+			case SHUTDOWN_CONTINUE:
+				poll_num = poll(fds, 1, 1);
+				if (poll_num == -1) {
+					if (errno == EINTR)
+						continue;
+					error_errno(_("polling inotify message stream failed"));
+					state->listen_data->shutdown = SHUTDOWN_ERROR;
+					continue;
+				}
+
+				if ((time(NULL) - checked) >= interval) {
+					checked = time(NULL);
+					if (check_stale_dir_renames(&state->listen_data->renames,
+						checked - interval)) {
+						trace_printf_key(&trace_fsmonitor,
+							"Missed IN_MOVED_TO events, forcing shutdown");
+						state->listen_data->shutdown = SHUTDOWN_FORCE;
+						continue;
+					}
+				}
+
+				if (poll_num > 0 && (fds[0].revents & POLLIN))
+					handle_events(state);
+
+				continue;
+			case SHUTDOWN_ERROR:
+				state->listen_error_code = -1;
+				ipc_server_stop_async(state->ipc_server_data);
+				break;
+			case SHUTDOWN_FORCE:
+				state->listen_error_code = 0;
+				ipc_server_stop_async(state->ipc_server_data);
+				break;
+			case SHUTDOWN_STOP:
+			default:
+				state->listen_error_code = 0;
+				break;
+		}
+		return;
+	}
+}
Signature · 1 line
gitgitgadget
#35 lore raw

PATCH v2 8/12 fsmonitor: determine if filesystem is local or remote

In reply to Eric DeCosta via GitGitGadget

From: Eric DeCosta <[email protected]>

Compare the given path to the mounted filesystems. Find the mount that is the longest prefix of the path (if any) and determine if that mount is on a local or remote filesystem.

---
 compat/fsmonitor/fsm-path-utils-linux.c | 169 ++++++++++++++++++++++++
 1 file changed, 169 insertions(+)
 create mode 100644 compat/fsmonitor/fsm-path-utils-linux.c
compat/fsmonitor/fsm-path-utils-linux.c new file +169 -0
new file mode 100644
index 00000000000..039f044b670
@@ -0,0 +1,169 @@
+#include "fsmonitor.h"
+#include "fsmonitor-path-utils.h"
+#include <errno.h>
+#include <mntent.h>
+#include <sys/mount.h>
+#include <sys/statvfs.h>
+
+/*
+ * https://github.com/coreutils/gnulib/blob/master/lib/mountlist.c
+ */
+#ifndef ME_REMOTE
+/* A file system is "remote" if its Fs_name contains a ':'
+   or if (it is of type (smbfs or cifs) and its Fs_name starts with '//')
+   or if it is of any other of the listed types
+   or Fs_name is equal to "-hosts" (used by autofs to mount remote fs).
+   "VM" file systems like prl_fs or vboxsf are not considered remote here. */
+# define ME_REMOTE(Fs_name, Fs_type)            \
+	(strchr (Fs_name, ':') != NULL              \
+	 || ((Fs_name)[0] == '/'                    \
+		 && (Fs_name)[1] == '/'                 \
+		 && (strcmp (Fs_type, "smbfs") == 0     \
+			 || strcmp (Fs_type, "smb3") == 0   \
+			 || strcmp (Fs_type, "cifs") == 0)) \
+	 || strcmp (Fs_type, "acfs") == 0           \
+	 || strcmp (Fs_type, "afs") == 0            \
+	 || strcmp (Fs_type, "coda") == 0           \
+	 || strcmp (Fs_type, "auristorfs") == 0     \
+	 || strcmp (Fs_type, "fhgfs") == 0          \
+	 || strcmp (Fs_type, "gpfs") == 0           \
+	 || strcmp (Fs_type, "ibrix") == 0          \
+	 || strcmp (Fs_type, "ocfs2") == 0          \
+	 || strcmp (Fs_type, "vxfs") == 0           \
+	 || strcmp ("-hosts", Fs_name) == 0)
+#endif
+
+static int find_mount(const char *path, const struct statvfs *fs,
+	struct mntent *ent)
+{
+	const char *const mounts = "/proc/mounts";
+	const char *rp = real_pathdup(path, 1);
+	struct mntent *ment = NULL;
+	struct statvfs mntfs;
+	FILE *fp;
+	int found = 0;
+	int dlen, plen, flen = 0;
+
+	ent->mnt_fsname = NULL;
+	ent->mnt_dir = NULL;
+	ent->mnt_type = NULL;
+
+	fp = setmntent(mounts, "r");
+	if (!fp) {
+		error_errno(_("setmntent('%s') failed"), mounts);
+		return -1;
+	}
+
+	plen = strlen(rp);
+
+	/* read all the mount information and compare to path */
+	while ((ment = getmntent(fp)) != NULL) {
+		if (statvfs(ment->mnt_dir, &mntfs)) {
+			switch (errno) {
+			case EPERM:
+			case ESRCH:
+			case EACCES:
+				continue;
+			default:
+				error_errno(_("statvfs('%s') failed"), ment->mnt_dir);
+				endmntent(fp);
+				return -1;
+			}
+		}
+
+		/* is mount on the same filesystem and is a prefix of the path */
+		if ((fs->f_fsid == mntfs.f_fsid) &&
+			!strncmp(ment->mnt_dir, rp, strlen(ment->mnt_dir))) {
+			dlen = strlen(ment->mnt_dir);
+			if (dlen > plen)
+				continue;
+			/*
+			 * root is always a potential match; otherwise look for
+			 * directory prefix
+			 */
+			if ((dlen == 1 && ment->mnt_dir[0] == '/') ||
+				(dlen > flen && (!rp[dlen] || rp[dlen] == '/'))) {
+				flen = dlen;
+				/*
+				 * https://man7.org/linux/man-pages/man3/getmntent.3.html
+				 *
+				 * The pointer points to a static area of memory which is
+				 * overwritten by subsequent calls to getmntent().
+				 */
+				found = 1;
+				free(ent->mnt_fsname);
+				free(ent->mnt_dir);
+				free(ent->mnt_type);
+				ent->mnt_fsname = xstrdup(ment->mnt_fsname);
+				ent->mnt_dir = xstrdup(ment->mnt_dir);
+				ent->mnt_type = xstrdup(ment->mnt_type);
+			}
+		}
+	}
+	endmntent(fp);
+
+	if (!found)
+		return -1;
+
+	return 0;
+}
+
+int fsmonitor__get_fs_info(const char *path, struct fs_info *fs_info)
+{
+	struct mntent ment;
+	struct statvfs fs;
+
+	if (statvfs(path, &fs))
+		return error_errno(_("statvfs('%s') failed"), path);
+
+
+	if (find_mount(path, &fs, &ment) < 0) {
+		free(ment.mnt_fsname);
+		free(ment.mnt_dir);
+		free(ment.mnt_type);
+		return -1;
+	}
+
+	trace_printf_key(&trace_fsmonitor,
+			 "statvfs('%s') [flags 0x%08lx] '%s' '%s'",
+			 path, fs.f_flag, ment.mnt_type, ment.mnt_fsname);
+
+	fs_info->is_remote = ME_REMOTE(ment.mnt_fsname, ment.mnt_type);
+	fs_info->typename = ment.mnt_fsname;
+	free(ment.mnt_dir);
+	free(ment.mnt_type);
+
+	trace_printf_key(&trace_fsmonitor,
+				"'%s' is_remote: %d",
+				path, fs_info->is_remote);
+	return 0;
+}
+
+int fsmonitor__is_fs_remote(const char *path)
+{
+	struct fs_info fs;
+
+	if (fsmonitor__get_fs_info(path, &fs))
+		return -1;
+
+	free(fs.typename);
+
+	return fs.is_remote;
+}
+
+/*
+ * No-op for now.
+ */
+int fsmonitor__get_alias(const char *path, struct alias_info *info)
+{
+	return 0;
+}
+
+/*
+ * No-op for now.
+ */
+char *fsmonitor__resolve_alias(const char *path,
+	const struct alias_info *info)
+{
+	return NULL;
+}
Signature · 1 line
gitgitgadget
#40 lore raw

PATCH v2 12/12 fsmonitor: update doc for Linux

In reply to Eric DeCosta via GitGitGadget

From: Eric DeCosta <[email protected]>

Update the documentation for Linux.

---
 Documentation/config/fsmonitor--daemon.txt |  4 ++--
 Documentation/git-fsmonitor--daemon.txt    | 24 ++++++++++++++--------
 2 files changed, 18 insertions(+), 10 deletions(-)
Documentation/config/fsmonitor--daemon.txt +2 -2
index c225c6c9e74..2cafb040d96 100644
@@ -4,8 +4,8 @@ fsmonitor.allowRemote::
     behavior.  Only respected when `core.fsmonitor` is set to `true`.
 
 fsmonitor.socketDir::
-    This Mac OS-specific option, if set, specifies the directory in
+    Mac OS and Linux-specific option. If set, specifies the directory in
     which to create the Unix domain socket used for communication
     between the fsmonitor daemon and various Git commands. The directory must
-    reside on a native Mac OS filesystem.  Only respected when `core.fsmonitor`
+    reside on a native filesystem.  Only respected when `core.fsmonitor`
     is set to `true`.
Documentation/git-fsmonitor--daemon.txt +16 -8
index 8238eadb0e1..c2b08229c74 100644
@@ -76,23 +76,31 @@ repositories; this may be overridden by setting `fsmonitor.allowRemote` to
 correctly with all network-mounted repositories and such use is considered
 experimental.
 
-On Mac OS, the inter-process communication (IPC) between various Git
+On Linux and Mac OS, the inter-process communication (IPC) between various Git
 commands and the fsmonitor daemon is done via a Unix domain socket (UDS) -- a
-special type of file -- which is supported by native Mac OS filesystems,
-but not on network-mounted filesystems, NTFS, or FAT32.  Other filesystems
-may or may not have the needed support; the fsmonitor daemon is not guaranteed
-to work with these filesystems and such use is considered experimental.
+special type of file -- which is supported by many native Linux and Mac OS
+filesystems, but not on network-mounted filesystems, NTFS, or FAT32.  Other
+filesystems may or may not have the needed support; the fsmonitor daemon is not
+guaranteed to work with these filesystems and such use is considered
+experimental.
 
 By default, the socket is created in the `.git` directory, however, if the
 `.git` directory is on a network-mounted filesystem, it will be instead be
 created at `$HOME/.git-fsmonitor-*` unless `$HOME` itself is on a
 network-mounted filesystem in which case you must set the configuration
-variable `fsmonitor.socketDir` to the path of a directory on a Mac OS native
+variable `fsmonitor.socketDir` to the path of a directory on a native
 filesystem in which to create the socket file.
 
 If none of the above directories (`.git`, `$HOME`, or `fsmonitor.socketDir`)
-is on a native Mac OS file filesystem the fsmonitor daemon will report an
-error that will cause the daemon and the currently running command to exit.
+is on a native Linux or Mac OS filesystem the fsmonitor daemon will report
+an error that will cause the daemon to exit and the currently running command
+to issue a warning.
+
+On Linux, the fsmonitor daemon registers a watch for each directory in the
+repository.  The default per-user limit for the number of watches on most Linux
+systems is 8192.  This may not be sufficient for large repositories or if
+multiple instances of the fsmonitor daemon are running.
+See https://watchexec.github.io/docs/inotify-limits.html[Linux inotify limits] for more information.
 
 CONFIGURATION
 -------------
Signature · 1 line
gitgitgadget
#41 lore raw
In reply to Eric DeCosta via GitGitGadget
Quoted 6 lines · Eric DeCosta via GitGitGadget

Goal is to deliver fsmonitor for Linux that is on par with fsmonitor for Windows and Mac OS.

This patch set builds upon previous work for done for Windows and Mac OS (first 6 patches) to implement a fsmonitor back-end for Linux based on the Linux inotify API.

Again, the first six patches are a part of what is queued as ed/fsmonitor-on-networked-macos that is now in 'next' but lacks a fix-up commit from Jeff King.

I understand that it might not be easy/possible (e.g. perhaps it is a limitation of GGG?), but I really prefer not to see them re-posted as part of this series, as I have to apply them and make sure there are no changes from the last one before discarding them.

Anyway, thanks for an update. Will requeue.

#32 lore raw

Re: [PATCH v2 07/12] fsmonitor: prepare to share code between Mac OS and Linux

In reply to Eric DeCosta via GitGitGadget
Quoted 10 lines · Eric DeCosta via GitGitGadget

ifdef FSMONITOR_DAEMON_BACKEND

Makefile +5 -1
 	COMPAT_CFLAGS += -DHAVE_FSMONITOR_DAEMON_BACKEND
+	ifdef FSMONITOR_DAEMON_COMMON
+		COMPAT_OBJS += compat/fsmonitor/fsm-ipc-$(FSMONITOR_DAEMON_COMMON).o
+	else
+		COMPAT_OBJS += compat/fsmonitor/fsm-ipc-$(FSMONITOR_DAEMON_BACKEND).o
+	endif
 	COMPAT_OBJS += compat/fsmonitor/fsm-listen-$(FSMONITOR_DAEMON_BACKEND).o
 	COMPAT_OBJS += compat/fsmonitor/fsm-health-$(FSMONITOR_DAEMON_BACKEND).o
-	COMPAT_OBJS += compat/fsmonitor/fsm-ipc-$(FSMONITOR_DAEMON_BACKEND).o

Makefile endif · line 2055

Quoted 10 lines
Makefile +3 -0
 endif
 
 ifdef FSMONITOR_OS_SETTINGS
 	COMPAT_CFLAGS += -DHAVE_FSMONITOR_OS_SETTINGS
+	ifdef FSMONITOR_DAEMON_COMMON
+		COMPAT_OBJS += compat/fsmonitor/fsm-settings-$(FSMONITOR_DAEMON_COMMON).o
+	endif
 	COMPAT_OBJS += compat/fsmonitor/fsm-settings-$(FSMONITOR_OS_SETTINGS).o
 	COMPAT_OBJS += compat/fsmonitor/fsm-path-utils-$(FSMONITOR_OS_SETTINGS).o
 endif

Ugly.

One overrides backend with common, while the other one doesn't. That asymmetry alone should stop us and wonder if there is something fishy in the approach that can be improved. It makes it look like the word "common" means something quite different between the -ipc and the -settings world.

I suspect that in both, you should not expose "unix" to this part of the Makefile. Linux and macOS occasionally being similar in some places does not have to be exposed here. INstead you can use backend "linux" and "macos", whose C sources may include from a separate C source file whose name may contain "unix". That would allow you to get rid of FSMONITOR_DAEMON_COMMON in a cleaner way.

#33 lore raw

RE: [PATCH v2 07/12] fsmonitor: prepare to share code between Mac OS and Linux

Quoted 51 lines

-----Original Message-----
From: Junio C Hamano <[email protected]>
Sent: Friday, October 14, 2022 7:46 PM
To: Eric DeCosta via GitGitGadget <[email protected]>
Cc: [email protected]; Eric Sunshine <[email protected]>; Ævar Arnfjörð Bjarmason <[email protected]>; Eric DeCosta <[email protected]>
Subject: Re: [PATCH v2 07/12] fsmonitor: prepare to share code between Mac OS and Linux

Makefile endif

Quoted 4 lines · Eric DeCosta via GitGitGadget

ifdef FSMONITOR_DAEMON_BACKEND

Makefile +2 -0
 	COMPAT_CFLAGS += -DHAVE_FSMONITOR_DAEMON_BACKEND
+	ifdef FSMONITOR_DAEMON_COMMON
+		COMPAT_OBJS += compat/fsmonitor/fsm-ipc-

$(FSMONITOR_DAEMON_COMMON).o

Review

Quoted 2 lines
(quoted diff) +2 -0
+	else
+		COMPAT_OBJS += compat/fsmonitor/fsm-ipc-

$(FSMONITOR_DAEMON_BACKEND).o

Review

Quoted 2 lines
(quoted diff) +1 -0
+	endif
 	COMPAT_OBJS += compat/fsmonitor/fsm-listen-

$(FSMONITOR_DAEMON_BACKEND).o

Quoted 1 line

COMPAT_OBJS += compat/fsmonitor/fsm-health-

$(FSMONITOR_DAEMON_BACKEND).o

Review

Quoted 1 line
(quoted diff) +0 -1
-	COMPAT_OBJS += compat/fsmonitor/fsm-ipc-

$(FSMONITOR_DAEMON_BACKEND).o

Makefile endif

Quoted 6 lines

endif

ifdef FSMONITOR_OS_SETTINGS

Makefile +2 -0
 	COMPAT_CFLAGS += -DHAVE_FSMONITOR_OS_SETTINGS
+	ifdef FSMONITOR_DAEMON_COMMON
+		COMPAT_OBJS += compat/fsmonitor/fsm-settings-

$(FSMONITOR_DAEMON_COMMON).o

Review

Quoted 2 lines
(quoted diff) +1 -0
+	endif
 	COMPAT_OBJS += compat/fsmonitor/fsm-settings-

$(FSMONITOR_OS_SETTINGS).o

Quoted 3 lines

COMPAT_OBJS +=
compat/fsmonitor/fsm-path-utils-$(FSMONITOR_OS_SETTINGS).o
endif

Ugly.

One overrides backend with common, while the other one doesn't. That asymmetry alone should stop us and wonder if there is something fishy in the approach that can be improved. It makes it look like the word "common" means something quite different between the -ipc and the - settings world.

Quoted 6 lines

I suspect that in both, you should not expose "unix" to this part of the Makefile. Linux and macOS occasionally being similar in some places does not have to be exposed here. INstead you can use backend "linux" and "macos", whose C sources may include from a separate C source file whose name may contain "unix". That would allow you to get rid of FSMONITOR_DAEMON_COMMON in a cleaner way.

Let me see if I am understanding you correctly. Are you suggesting something like:

ifdef FSMONITOR_DAEMON_BACKEND_LINUX

	COMPAT_CFLAGS += -DHAVE_FSMONITOR_DAEMON_BACKEND
	COMPAT_OBJS += compat/fsmonitor/fsm-ipc-unix.o
	COMPAT_OBJS += compat/fsmonitor/fsm-ipc-$(FSMONITOR_DAEMON_BACKEND_LINUX).o
	COMPAT_OBJS += compat/fsmonitor/fsm-listen-$(FSMONITOR_DAEMON_BACKEND_LINUX).o
	COMPAT_OBJS += compat/fsmonitor/fsm-health-$(FSMONITOR_DAEMON_BACKEND_LINUX).o

endif

ifdef FSMONITOR_DAEMON_BACKEND_DARWIN

	COMPAT_CFLAGS += -DHAVE_FSMONITOR_DAEMON_BACKEND
	COMPAT_OBJS += compat/fsmonitor/fsm-ipc-unix.o
	COMPAT_OBJS += compat/fsmonitor/fsm-ipc-$(FSMONITOR_DAEMON_BACKEND_DARWIN).o
	COMPAT_OBJS += compat/fsmonitor/fsm-listen-$(FSMONITOR_DAEMON_BACKEND_DARWIN).o
	COMPAT_OBJS += compat/fsmonitor/fsm-health-$(FSMONITOR_DAEMON_BACKEND_DARWIN).o

endif

And similarly for settings:

ifdef FSMONITOR_DAEMON_SETTINGS_LINUX

	COMPAT_CFLAGS += -DHAVE_FSMONITOR_OS_SETTINGS
	COMPAT_OBJS += compat/fsmonitor/fsm-settings-unix.o
	COMPAT_OBJS += compat/fsmonitor/fsm-settings-$(FSMONITOR_DAEMON_SETTINGS_LINUX).o
	COMPAT_OBJS += compat/fsmonitor/fsm-path-utils-$(FSMONITOR_DAEMON_SETTINGS_LINUX).o

endif

ifdef FSMONITOR_DAEMON_SETTINGS_DARWIN

	COMPAT_CFLAGS += -DHAVE_FSMONITOR_OS_SETTINGS
	COMPAT_OBJS += compat/fsmonitor/fsm-settings-unix.o
	COMPAT_OBJS += compat/fsmonitor/fsm-settings-$(FSMONITOR_DAEMON_SETTINGS_DARWIN).o
	COMPAT_OBJS += compat/fsmonitor/fsm-path-utils-$(FSMONITOR_DAEMON_SETTINGS_DARWIN).o

endif

And the rest of the platforms remain as was, with FSMONITOR_DAEMON_BACKEND and FSMONITOR_OS_SETTINGS

-Eric

#42 lore raw
In reply to Junio C Hamano
Quoted 24 lines

-----Original Message-----
From: Junio C Hamano <[email protected]>
Sent: Friday, October 14, 2022 7:33 PM
To: Eric DeCosta via GitGitGadget <[email protected]>
Cc: [email protected]; Eric Sunshine <[email protected]>; Ævar Arnfjörð Bjarmason <[email protected]>; Eric DeCosta <[email protected]>
Subject: Re: [PATCH v2 00/12] fsmonitor: Implement fsmonitor for Linux

Quoted 6 lines · Eric DeCosta via GitGitGadget

Goal is to deliver fsmonitor for Linux that is on par with fsmonitor for Windows and Mac OS.

This patch set builds upon previous work for done for Windows and Mac OS (first 6 patches) to implement a fsmonitor back-end for Linux based on the Linux inotify API.

Again, the first six patches are a part of what is queued as ed/fsmonitor-on- networked-macos that is now in 'next' but lacks a fix-up commit from Jeff King.

Quoted 6 lines

I understand that it might not be easy/possible (e.g. perhaps it is a limitation of GGG?), but I really prefer not to see them re-posted as part of this series, as I have to apply them and make sure there are no changes from the last one before discarding them.

Anyway, thanks for an update. Will requeue.

I looks like I can use GGG with the next branch, but I will have to open a new PR (and close the existing one).

-Eric

#45 lore raw
In reply to Eric DeCosta via GitGitGadget

At $DAYJOB, we observed that this topic breaks MacOS builds with sha1dc:

  $ make NO_APPLE_COMMON_CRYPTO=1 DC_SHA1=1 NO_OPENSSL=1 compat/fsmonitor/fsm-ipc-darwin.o  
      CC compat/fsmonitor/fsm-ipc-darwin.o
    compat/fsmonitor/fsm-ipc-darwin.c:13:2: error: unknown type name 'SHA_CTX'; did you mean 'SHA1_CTX'?
            SHA_CTX sha1ctx;
            ^~~~~~~
            SHA1_CTX
    ./sha1dc/sha1.h:55:3: note: 'SHA1_CTX' declared here
    } SHA1_CTX;
      ^
    compat/fsmonitor/fsm-ipc-darwin.c:16:21: error: use of undeclared identifier 'SHA_DIGEST_LENGTH'
            unsigned char hash[SHA_DIGEST_LENGTH];
                              ^
    compat/fsmonitor/fsm-ipc-darwin.c:31:2: error: implicit declaration of function 'SHA1_Init' is invalid in C99 [-Werror,-Wimplicit-function-declaration]
            SHA1_Init(&sha1ctx);
            ^
    compat/fsmonitor/fsm-ipc-darwin.c:31:2: note: did you mean 'SHA1DCInit'?
    ./sha1dc/sha1.h:58:6: note: 'SHA1DCInit' declared here
    void SHA1DCInit(SHA1_CTX*);
        ^
    compat/fsmonitor/fsm-ipc-darwin.c:32:2: error: implicit declaration of function 'SHA1_Update' is invalid in C99 [-Werror,-Wimplicit-function-declaration]
            SHA1_Update(&sha1ctx, r->worktree, strlen(r->worktree));
            ^
    compat/fsmonitor/fsm-ipc-darwin.c:32:2: note: did you mean 'SHA1DCUpdate'?
    ./sha1dc/sha1.h:96:6: note: 'SHA1DCUpdate' declared here
    void SHA1DCUpdate(SHA1_CTX*, const char*, size_t);
        ^
    compat/fsmonitor/fsm-ipc-darwin.c:33:2: error: implicit declaration of function 'SHA1_Final' is invalid in C99 [-Werror,-Wimplicit-function-declaration]
            SHA1_Final(hash, &sha1ctx);
            ^
    compat/fsmonitor/fsm-ipc-darwin.c:33:2: note: did you mean 'SHA1DCFinal'?
    ./sha1dc/sha1.h:100:6: note: 'SHA1DCFinal' declared here
    int  SHA1DCFinal(unsigned char[20], SHA1_CTX*);
        ^
    5 errors generated.
    make: *** [compat/fsmonitor/fsm-ipc-darwin.o] Error 1

Without NO_OPENSSL, this still fails, but with slightly different error messages.

  $ make NO_APPLE_COMMON_CRYPTO=1 DC_SHA1=1 compat/fsmonitor/fsm-ipc-darwin.o

CC compat/fsmonitor/fsm-ipc-darwin.o
compat/fsmonitor/fsm-ipc-darwin.c:31:2: error: 'SHA1_Init' is deprecated [-Werror,-Wdeprecated-declarations]
SHA1_Init(&sha1ctx);
^
/opt/local/include/openssl/sha.h:49:1: note: 'SHA1_Init' has been explicitly marked deprecated here OSSL_DEPRECATEDIN_3_0 int SHA1_Init(SHA_CTX *c);
^
/opt/local/include/openssl/macros.h:182:49: note: expanded from macro 'OSSL_DEPRECATEDIN_3_0'
# define OSSL_DEPRECATEDIN_3_0 OSSL_DEPRECATED(3.0)
^
/opt/local/include/openssl/macros.h:62:52: note: expanded from macro 'OSSL_DEPRECATED'
# define OSSL_DEPRECATED(since) __attribute__((deprecated))
^
compat/fsmonitor/fsm-ipc-darwin.c:32:2: error: 'SHA1_Update' is deprecated [-Werror,-Wdeprecated-declarations]
SHA1_Update(&sha1ctx, r->worktree, strlen(r->worktree));
^
/opt/local/include/openssl/sha.h:50:1: note: 'SHA1_Update' has been explicitly marked deprecated here OSSL_DEPRECATEDIN_3_0 int SHA1_Update(SHA_CTX *c, const void *data, size_t len);
^
/opt/local/include/openssl/macros.h:182:49: note: expanded from macro 'OSSL_DEPRECATEDIN_3_0'
# define OSSL_DEPRECATEDIN_3_0 OSSL_DEPRECATED(3.0)
^
/opt/local/include/openssl/macros.h:62:52: note: expanded from macro 'OSSL_DEPRECATED'
# define OSSL_DEPRECATED(since) __attribute__((deprecated))
^
compat/fsmonitor/fsm-ipc-darwin.c:33:2: error: 'SHA1_Final' is deprecated [-Werror,-Wdeprecated-declarations]
SHA1_Final(hash, &sha1ctx);
^
/opt/local/include/openssl/sha.h:51:1: note: 'SHA1_Final' has been explicitly marked deprecated here OSSL_DEPRECATEDIN_3_0 int SHA1_Final(unsigned char *md, SHA_CTX *c);
^
/opt/local/include/openssl/macros.h:182:49: note: expanded from macro 'OSSL_DEPRECATEDIN_3_0'
# define OSSL_DEPRECATEDIN_3_0 OSSL_DEPRECATED(3.0)
^
/opt/local/include/openssl/macros.h:62:52: note: expanded from macro 'OSSL_DEPRECATED'
# define OSSL_DEPRECATED(since) __attribute__((deprecated))
^
3 errors generated.
make: *** [compat/fsmonitor/fsm-ipc-darwin.o] Error 1

Quoted 20 lines · Eric DeCosta via GitGitGadget

Goal is to deliver fsmonitor for Linux that is on par with fsmonitor for Windows and Mac OS.

This patch set builds upon previous work for done for Windows and Mac OS (first 6 patches) to implement a fsmonitor back-end for Linux based on the Linux inotify API. inotify differs significantly from the equivalent Windows and Mac OS APIs in that a watch must be registered for every directory of interest (rather than a singular watch at the root of the directory tree) and special care must be taken to handle directory renames correctly.

More information about inotify:
https://man7.org/linux/man-pages/man7/inotify.7.html

v1 differs from v0:

* Code review feedback
* Update how and which code can be shared between Mac OS and Linux
* Increase polling frequency to every 1ms (matches Mac OS)
* Updates to t7527 to improve test stability

#43 lore raw
In reply to Eric DeCosta
Quoted 9 lines · Eric DeCosta
Quoted 6 lines

I understand that it might not be easy/possible (e.g. perhaps it is a limitation of GGG?), but I really prefer not to see them re-posted as part of this series, as I have to apply them and make sure there are no changes from the last one before discarding them.

Anyway, thanks for an update. Will requeue.

I looks like I can use GGG with the next branch, but I will have to open a new PR (and close the existing one).

Please don't.

The macOS topic (thanks for working on it, by the way) is about to graduate to 'master', and the situation would probably "improve" anyway when it happens, I hope.

Thanks.

#46 lore raw
In reply to Glen Choo
Quoted 1 line · Glen Choo

At $DAYJOB, we observed that this topic breaks MacOS builds with sha1dc:

Thanks for a report.

How dissapointing. The thing is that the topic has been in 'next' since the 11th (i.e. early last week), and I know that you guys rely on the tip of 'next' in working order to cut your internal releases, but we did not hear about this until now. What makes it taste even worse is that nobody else caught this, even though we seem to have a couple of macOS jobs at GitHub Actions CI, there we didn't see any breakage related to this.

Possible action items:

* See what configurations our two macOS jobs are using. If neither is using sha1dc, I would say that is criminal [*] and at least one of them should be updated to do so right away.

* The "two macOS CI jobs sharing too many configuration knobs" may not be limited to just SHA-1 implementation, but unlike Linux builds and tests, we may have similar "monoculture" issue in our macOS CI builds. Those users, who depend on macOS port being healthy, should help identify unnecessary overlaps between the two, and more importantly, make sure we have CI builds with configuration similar to what they actually use.

* Adding a few build-only-without-tests CI jobs also might help.

* Those who depend on working macOS port, especially those with corporate backing who choose to use configurations that are different from what we have CI builds for, are requested to arrange a more frequent build test to catch a problem like this much earlier.

Anything else I forgot that we can do to improve the situation? I personally do not use macOS, I am not interested in using one, but I do value those who choose to use macOS have happy git working on their platform, so the stakeholders need to chip in.

Thanks.

[Footnote]

* Until the world migrates over to SHA-256, the collision detecting SHA-1 implementation is what we must use unless there is a strong reason not to. If we are not testing something that ought to be the default, we are not doing a very good job.

#34 lore raw

Re: [PATCH v2 07/12] fsmonitor: prepare to share code between Mac OS and Linux

In reply to Eric DeCosta
Quoted 8 lines · Eric DeCosta
Quoted 8 lines

Ugly.

One overrides backend with common, while the other one doesn't. That asymmetry alone should stop us and wonder if there is something fishy in the approach that can be improved. It makes it look like the word "common" means something quite different between the -ipc and the - settings world.

Quoted 8 lines
Quoted 6 lines

I suspect that in both, you should not expose "unix" to this part of the Makefile. Linux and macOS occasionally being similar in some places does not have to be exposed here. INstead you can use backend "linux" and "macos", whose C sources may include from a separate C source file whose name may contain "unix". That would allow you to get rid of FSMONITOR_DAEMON_COMMON in a cleaner way.

Let me see if I am understanding you correctly. Are you suggesting something like:

Not really.

What I had in mind was an arrangement more like

    $ for variant in linux macos; do grep include fsm-ipc-$variant.c; done
    #include "fsm-ipc-common-unix.cinclude"
    #include "fsm-ipc-common-unix.cinclude"

and COMPAT_OBJS knowning only about fsm-ipc-linux.o or fsm-ipc-macos.o, depending on which platform you are building.

#37 lore raw

Re: [PATCH v2 09/12] fsmonitor: implement filesystem change listener for Linux

In reply to Eric DeCosta via GitGitGadget

compat/fsmonitor/fsm-listen-linux.c line 583

Quoted 5 lines · Eric DeCosta via GitGitGadget

From: Eric DeCosta <[email protected]>
[...]

compat/fsmonitor/fsm-listen-linux.c new file +3 -0
+			strbuf_add(&path, w->dir, strlen(w->dir));
+			strbuf_addch(&path, '/');
+			strbuf_add(&path, event->name,  strlen(event->name));

Don't do strbuf_add(&buf, x, strlen(x), just use strbuf_addstr(&buf, x) instead.

The same goes for a couple of existing occurances that hit "master" already in the just-merged fsmonitor topic, but in this case we can change it in-flight still. Thanks!

#26 lore raw

Re: [PATCH v2 02/12] fsmonitor: relocate socket file if .git directory is remote

In reply to Eric DeCosta via GitGitGadget

compat/fsmonitor/fsm-ipc-darwin.c line 12

Quoted 5 lines · Eric DeCosta via GitGitGadget

From: Eric DeCosta <[email protected]>
[..]

compat/fsmonitor/fsm-ipc-darwin.c new file +3 -0
+const char *fsmonitor_ipc__get_path(struct repository *r)
+{
+	static const char *ipc_path = NULL;

Urm, I see some of this interface made it into master already with 6beb2688d33 (fsmonitor: relocate socket file if .git directory is remote, 2022-10-04), but this is just weird. We already have "struct fsmonitor_settings" which "lazily loads" these various settings.

So e.g. in refresh_fsmonitor() we might get the hook path, which we "lazy load" and cache there.

And here we have an extra layer of another type of lazy loading, and this will e.g. be called from fsmonitor-settings.c.

Is there really a good reason for why the relevant codepaths can't either not-lazy-load this (e.g. when we get the socket), or why we can't just use fsmonitor-settings.c's existing caching?

Just wondering...

compat/fsmonitor/fsm-ipc-darwin.c line 44

Quoted 10 lines
compat/fsmonitor/fsm-ipc-darwin.c new file +10 -0
+	repo_config_get_string(r, "fsmonitor.socketdir", &sock_dir);
+
+	/* Create the socket file in either socketDir or $HOME */
+	if (sock_dir && *sock_dir) {
+		strbuf_addf(&ipc_file, "%s/.git-fsmonitor-%s",
+					sock_dir, hash_to_hex(hash));
+	} else {
+		strbuf_addf(&ipc_file, "~/.git-fsmonitor-%s", hash_to_hex(hash));
+	}
+	free(sock_dir);

Instead of this init sock_dir to NULL & then check if it's non-NULL etc. Just do:

	char *sock_dir;

	if (!repo_config_get_string_tmp(r, "fsmonitor.socketdir", &sock_dir) &&
	    *sock_dir)
		...
	else
		...

Note also the *_tmp(), then you won't have to free() it.

compat/fsmonitor/fsm-ipc-darwin.c line 51

Quoted 7 lines
compat/fsmonitor/fsm-ipc-darwin.c new file +7 -0
+
+	ipc_path = interpolate_path(ipc_file.buf, 1);
+	if (!ipc_path)
+		die(_("Invalid path: %s"), ipc_file.buf);
+
+	strbuf_release(&ipc_file);
+	return ipc_path;

Why can't we just do something more simpler like this in the *nix case?

I think because you're creating this socket for a NFS-mounted repo, so of course we might have multiple clients.

But you also seem to be making it "unique" by hashing r->worktree, which is the path to the repo work tree. At least in my use-cases for NFS mounted repos (which have been limited) all of those paths would be the same for me across my systems, because I'd have them at /home/avar/g/some-git-tree/...

In builtin/gc.c we create a "unique for system" lock file using xgethostname(), isn't that a lot simpler & closer to what you actually want?

In any case, some details about motivations, why etc. are sorely missing in the commit message. We can see *what* you're doing, but not *why*, or what other simpler solutions etc. might have been tried & dismissed...

#2 lore raw

Re: [PATCH 01/12] fsmonitor: refactor filesystem checks to common interface

In reply to Eric DeCosta via GitGitGadget
Quoted 11 lines · Eric DeCosta via GitGitGadget

From: Eric DeCosta <[email protected]>
[...]

compat/fsmonitor/fsm-path-utils-darwin.c new file +9 -0
+int fsmonitor__get_fs_info(const char *path, struct fs_info *fs_info)
+{
+	struct statfs fs;
+	if (statfs(path, &fs) == -1) {
+		int saved_errno = errno;
+		trace_printf_key(&trace_fsmonitor, "statfs('%s') failed: %s",
+				 path, strerror(saved_errno));
+		errno = saved_errno;
+		return -1;

compat/fsmonitor/fsm-path-utils-darwin.c line 24

Quoted 10 lines
compat/fsmonitor/fsm-path-utils-darwin.c new file +10 -0
+	}
+
+	trace_printf_key(&trace_fsmonitor,
+			 "statfs('%s') [type 0x%08x][flags 0x%08x] '%s'",
+			 path, fs.f_type, fs.f_flags, fs.f_fstypename);
+
+	if (!(fs.f_flags & MNT_LOCAL))
+		fs_info->is_remote = 1;
+	else
+		fs_info->is_remote = 0;

Odd to go through the trouble of inverting the flag check like this just to assign to it manually. I'd think:

	if (flags & LOCAL)
		is_remote = 0;
	else
		is_remote = 1;

Would make more sense, or actually if you do invert it:

	fs_info->is_remote = !(fs.f_flags & MNT_LOCAL);

No?

compat/fsmonitor/fsm-path-utils-darwin.c line 26

Quoted 1 line
compat/fsmonitor/fsm-path-utils-darwin.c new file +1 -0
+	fs_info->typename = xstrdup(fs.f_fstypename);

Instead of xstrdup()-ing this...

Quoted 4 lines
compat/fsmonitor/fsm-path-utils-darwin.c new file +4 -0
+
+	trace_printf_key(&trace_fsmonitor,
+				"'%s' is_remote: %d",
+				path, fs_info->is_remote);

compat/fsmonitor/fsm-path-utils-darwin.c line 40

Quoted 10 lines
compat/fsmonitor/fsm-path-utils-darwin.c new file +10 -0
+	return 0;
+}
+
+int fsmonitor__is_fs_remote(const char *path)
+{
+	struct fs_info fs;
+	if (fsmonitor__get_fs_info(path, &fs))
+		return -1;
+
+	free(fs.typename);

...just to have some callers free() it again, maybe it makes sense to say that return a const char *, and if you need it past the call to fsmonitor__get_fs_info() you should do the xstrdup() yourself.

Skimming the end-state the OSX one gives you a copy of your own, as it's stuck into the struct you provided.

For Linux we don't get a copy, but it's filled per getmntent() call.

Anyway, small potatoes...

compat/fsmonitor/fsm-path-utils-win32.c line 22

Quoted 5 lines
compat/fsmonitor/fsm-path-utils-win32.c new file +5 -0
+	if (h == INVALID_HANDLE_VALUE) {
+		error(_("[GLE %ld] unable to open for read '%ls'"),
+		      GetLastError(), wpath);
+		return -1;
+	}

Do away with the braces and just "return error(..."

compat/fsmonitor/fsm-path-utils-win32.c line 32

Quoted 10 lines
compat/fsmonitor/fsm-path-utils-win32.c new file +10 -0
+
+	if (!GetFileInformationByHandleEx(h, FileRemoteProtocolInfo,
+		&proto_info, sizeof(proto_info))) {
+		error(_("[GLE %ld] unable to get protocol information for '%ls'"),
+		      GetLastError(), wpath);
+		CloseHandle(h);
+		return -1;
+	}
+
+	CloseHandle(h);

And this duplication you can avoid with an earlier:

	int ret = 0;

Then

		ret = error(...)
		goto cleanup;

and then at the end:

cleanup:

	CloseHandle(h)
	return ret;

compat/fsmonitor/fsm-path-utils-win32.c line 81

Quoted 7 lines
compat/fsmonitor/fsm-path-utils-win32.c new file +7 -0
+	/*
+	 * Do everything in wide chars because the drive letter might be
+	 * a multi-byte sequence.  See win32_has_dos_drive_prefix().
+	 */
+	if (xutftowcs_path(wpath, path) < 0) {
+		return -1;
+	}

Don't need the braces.

Quoted 11 lines
compat/fsmonitor/fsm-path-utils-win32.c new file +11 -0
+
+	/*
+	 * GetDriveTypeW() requires a final slash.  We assume that the
+	 * worktree pathname points to an actual directory.
+	 */
+	wlen = wcslen(wpath);
+	if (wpath[wlen - 1] != L'\\' && wpath[wlen - 1] != L'/') {
+		wpath[wlen++] = L'\\';
+		wpath[wlen] = 0;
+	}
+

compat/fsmonitor/fsm-path-utils-win32.c line 102

Quoted 10 lines
compat/fsmonitor/fsm-path-utils-win32.c new file +10 -0
+	/*
+	 * Normalize the path.  If nothing else, this converts forward
+	 * slashes to backslashes.  This is essential to get GetDriveTypeW()
+	 * correctly handle some UNC "\\server\share\..." paths.
+	 */
+	if (!GetFullPathNameW(wpath, MAX_PATH, wfullpath, NULL)) {
+		return -1;
+	}
+
+	driveType = GetDriveTypeW(wfullpath);

Can't you just do away with this driveType variable and assign directly to fs_info->is_remote if it's == DRIVE_REMOTE?...

compat/fsmonitor/fsm-path-utils-win32.c line 105

Quoted 3 lines
compat/fsmonitor/fsm-path-utils-win32.c new file +3 -0
+	trace_printf_key(&trace_fsmonitor,
+			 "DriveType '%s' L'%ls' (%u)",
+			 path, wfullpath, driveType);

...well, not due to this, but do we really need to log the raw value & is that meaningful? A user of the trace2 logs would need to reverse map that, doesn't the API provide some "what is it then?" function?

compat/fsmonitor/fsm-path-utils-win32.c line 108

Quoted 3 lines
compat/fsmonitor/fsm-path-utils-win32.c new file +3 -0
+
+	if (driveType == DRIVE_REMOTE) {
+		fs_info->is_remote = 1;

Here we do that assignment...

compat/fsmonitor/fsm-path-utils-win32.c line 117

Quoted 9 lines
compat/fsmonitor/fsm-path-utils-win32.c new file +9 -0
+		if (check_remote_protocol(wfullpath) < 0)
+			return -1;
+	} else {
+		fs_info->is_remote = 0;
+	}
+
+	trace_printf_key(&trace_fsmonitor,
+				"'%s' is_remote: %d",
+				path, fs_info->is_remote);

ditto just "is remote?"

#17 lore raw

Re: [PATCH 11/12] fsmonitor: test updates

In reply to Eric DeCosta via GitGitGadget

t/t7527-builtin-fsmonitor.sh start_daemon () { · line 82

Quoted 8 lines · Eric DeCosta via GitGitGadget

From: Eric DeCosta <[email protected]>

t/t7527-builtin-fsmonitor.sh +6 -0
+wait_for_update () {
+	func=$1 &&
+	file=$2 &&
+	sz=$(wc -c < "$file") &&
+	last=0 &&
+	$func &&

Odd not to quote "$func" here, but anyway...

t/t7527-builtin-fsmonitor.sh create_files () { · line 403

Quoted 3 lines
t/t7527-builtin-fsmonitor.sh +3 -0
+rename_directory () {
+	mv dirtorename dirrenamed
+}

...uh, we need to wrap "mv"? Why?

t/t7527-builtin-fsmonitor.sh test_expect_success 'rename directory' ' · line 519

Quoted 2 lines
t/t7527-builtin-fsmonitor.sh +1 -1
-	mv dirtorename dirrenamed &&
+	wait_for_update rename_directory "$PWD/.git/trace" &&

Just to get around some quoting issue here?

#19 lore raw

Re: [PATCH 12/12] fsmonitor: update doc for Linux

In reply to Eric DeCosta via GitGitGadget
Quoted 15 lines · Eric DeCosta via GitGitGadget

From: Eric DeCosta <[email protected]>

Update the documentation for Linux.

---
 Documentation/config/fsmonitor--daemon.txt |  4 ++--
 Documentation/git-fsmonitor--daemon.txt    | 24 ++++++++++++++--------
 2 files changed, 18 insertions(+), 10 deletions(-)
Documentation/config/fsmonitor--daemon.txt +0 -0
index c225c6c9e74..2cafb040d96 100644

Documentation/config/fsmonitor--daemon.txt fsmonitor.allowRemote:: · line 11

Quoted 10 lines
Documentation/config/fsmonitor--daemon.txt +2 -2
@@ -4,8 +4,8 @@ fsmonitor.allowRemote::
     behavior.  Only respected when `core.fsmonitor` is set to `true`.
 
 fsmonitor.socketDir::
-    This Mac OS-specific option, if set, specifies the directory in
+    Mac OS and Linux-specific option. If set, specifies the directory in
     which to create the Unix domain socket used for communication
     between the fsmonitor daemon and various Git commands. The directory must
-    reside on a native Mac OS filesystem.  Only respected when `core.fsmonitor`
+    reside on a native filesystem.  Only respected when `core.fsmonitor`
     is set to `true`.

I think this should be squashed into the relevant commit(s) when we start supporting this fsmonitor.socketDir for that platform.

But on the content: Shouldn't we just reword this to say something like "*nix"-specific, we only have one platform which is likely to not have "sockets", i.e. Windows, no?

So rather than having an ever growing list of OS's that'll inevitably grow to something like:

Mac OS, Linux, FreeBSD, NetBSD, OpenBSD, GNU/Hurd-specific option, if set...

Let's find some way to refer to the platforms we do support, and instead mention the only one we don't support sockets on?

Documentation/git-fsmonitor--daemon.txt repositories; this may be overridden by setting `fsmonitor.allowRemote` to · line 79

Quoted 10 lines
Documentation/git-fsmonitor--daemon.txt +1 -1
index 8238eadb0e1..c2b08229c74 100644
@@ -76,23 +76,31 @@ repositories; this may be overridden by setting `fsmonitor.allowRemote` to
 correctly with all network-mounted repositories and such use is considered
 experimental.
 
-On Mac OS, the inter-process communication (IPC) between various Git
+On Linux and Mac OS, the inter-process communication (IPC) between various Git

Ditto.

#88 lore raw
In reply to Eric DeCosta via GitGitGadget
Quoted 2 lines · Eric DeCosta via GitGitGadget

Goal is to deliver fsmonitor for Linux that is on par with fsmonitor for Windows and Mac OS.

I applaud the effort, I gave this some light reviewing, I hope it helps.

Quoted 9 lines

This patch set builds upon previous work for done for Windows and Mac OS (first 6 patches) to implement a fsmonitor back-end for Linux based on the Linux inotify API. inotify differs significantly from the equivalent Windows and Mac OS APIs in that a watch must be registered for every directory of interest (rather than a singular watch at the root of the directory tree) and special care must be taken to handle directory renames correctly.

More information about inotify:
https://man7.org/linux/man-pages/man7/inotify.7.html

You haven't said why/if you considered using fanotify() instead of inotify(), which seems like a more natural target to me. It's unlikely that anyone who cares to use this isn't also using a new enough kernel.

See previous on-list discussions at:
https://lore.kernel.org/git/?q=fanotify

I think it would address some of the rac econditions you're mentioning here.

#44 lore raw
In reply to Eric DeCosta

Hi Eric,

Quoted 18 lines · Eric DeCosta
Quoted 18 lines

-----Original Message-----
From: Junio C Hamano <[email protected]>
Sent: Friday, October 14, 2022 7:33 PM
To: Eric DeCosta via GitGitGadget <[email protected]>
Cc: [email protected]; Eric Sunshine <[email protected]>; Ævar Arnfjörð Bjarmason <[email protected]>; Eric DeCosta <[email protected]>
Subject: Re: [PATCH v2 00/12] fsmonitor: Implement fsmonitor for Linux

Quoted 6 lines · Eric DeCosta via GitGitGadget

Goal is to deliver fsmonitor for Linux that is on par with fsmonitor for Windows and Mac OS.

This patch set builds upon previous work for done for Windows and Mac OS (first 6 patches) to implement a fsmonitor back-end for Linux based on the Linux inotify API.

Quoted 8 lines
Quoted 8 lines

Again, the first six patches are a part of what is queued as ed/fsmonitor-on- networked-macos that is now in 'next' but lacks a fix-up commit from Jeff King.

I understand that it might not be easy/possible (e.g. perhaps it is a limitation of GGG?), but I really prefer not to see them re-posted as part of this series, as I have to apply them and make sure there are no changes from the last one before discarding them.

GitGitGadget mirrors all branches from gitster/git to gitgitgadget/git, so if you open a PR in the latter repository, you can use all of those branches as targets.

But this PR is in git/git, which does not offer that.

Quoted 3 lines
Quoted 1 line

Anyway, thanks for an update. Will requeue.

I looks like I can use GGG with the next branch, but I will have to open a new PR (and close the existing one).

No, you do not have to open a new PR for that.

You just hit the Edit button on the top (as if you wanted to change the title) and then select the target ("base") branch in the drop-down box under the title.

Ciao,
Dscho

#47 lore raw
In reply to Junio C Hamano

Cc-ed Johannes, who would know a lot more about CI than I do.

Quoted 10 lines · Junio C Hamano
Quoted 1 line · Glen Choo

At $DAYJOB, we observed that this topic breaks MacOS builds with sha1dc:

Thanks for a report.

How dissapointing. The thing is that the topic has been in 'next' since the 11th (i.e. early last week), and I know that you guys rely on the tip of 'next' in working order to cut your internal releases, but we did not hear about this until now.

Yes. Unfortunately, we (Google's Git team) release on a weekly cadence; we merge on Fridays and build on Mondays (PST), which makes this timing extremely unfortunate.

Quoted 5 lines

Possible action items:

* See what configurations our two macOS jobs are using. If neither is using sha1dc, I would say that is criminal [*] and at least one of them should be updated to do so right away.

I'm not too familiar with the CI, but I took a quick peek at ci/lib.sh and noticed that none of the jobs build with sha1dc, not even the Linux or Windows ones, so..

Quoted 7 lines

* The "two macOS CI jobs sharing too many configuration knobs" may not be limited to just SHA-1 implementation, but unlike Linux builds and tests, we may have similar "monoculture" issue in our macOS CI builds. Those users, who depend on macOS port being healthy, should help identify unnecessary overlaps between the two, and more importantly, make sure we have CI builds with configuration similar to what they actually use.

I don't think this is a macOS-specific issue; our CI just doesn't do a good job with testing various build configurations.

Quoted 1 line

* Adding a few build-only-without-tests CI jobs also might help.

This sounds like the way to go IMO. It might be too expensive to run the full test suite on every build configuration, but build-without-test might be ok.

Quoted 5 lines

* Those who depend on working macOS port, especially those with corporate backing who choose to use configurations that are different from what we have CI builds for, are requested to arrange a more frequent build test to catch a problem like this much earlier.

I wished we had caught it sooner too. The folks here generally agree that our weekly release cycle is not ideal for reasons such as this. Hopefully this is good motivation to move that work forward, though I can't promise anything right now.

Quoted 4 lines

Anything else I forgot that we can do to improve the situation? I personally do not use macOS, I am not interested in using one, but I do value those who choose to use macOS have happy git working on their platform, so the stakeholders need to chip in.

There's nothing else I can think of at the moment. Thanks for your patience and for moving the conversation along.

Quoted 10 lines

Thanks.

[Footnote]

* Until the world migrates over to SHA-256, the collision detecting SHA-1 implementation is what we must use unless there is a strong reason not to. If we are not testing something that ought to be the default, we are not doing a very good job.

#48 lore raw
Quoted 4 lines · Glen Choo

I wished we had caught it sooner too. The folks here generally agree that our weekly release cycle is not ideal for reasons such as this. Hopefully this is good motivation to move that work forward, though I can't promise anything right now.

It is perfectly OK to have an automated trial build job that runs more frequently than your weekly release cycle, though. It should usually yield only a single bit of usable information (e.g. "there is no 'does not even build from the source' issue in upstream") that may give you assurance (e.g. "if we maintain the course, the next real build for release would hopefully go smoothly"), but when it breaks, you have more time to react.

Thanks.

#54 lore raw
In reply to Junio C Hamano
Quoted 6 lines · Junio C Hamano
Quoted 1 line · Glen Choo

At $DAYJOB, we observed that this topic breaks MacOS builds with sha1dc:

Thanks for a report.

Quoted 7 lines

How dissapointing. The thing is that the topic has been in 'next' since the 11th (i.e. early last week), and I know that you guys rely on the tip of 'next' in working order to cut your internal releases, but we did not hear about this until now. What makes it taste even worse is that nobody else caught this, even though we seem to have a couple of macOS jobs at GitHub Actions CI, there we didn't see any breakage related to this.

FWIW I see you caught it on the 9th in
https://lore.kernel.org/git/[email protected]/, but then the base topic was merged down on the 17th.

Quoted 5 lines

Possible action items:

* See what configurations our two macOS jobs are using. If neither is using sha1dc, I would say that is criminal [*] and at least one of them should be updated to do so right away.

I submitted a v2 of my series to finally make OSX use SHA1DC by default:
https://lore.kernel.org/git/[email protected]/

As part of that we'll CI that & the current "apple common crypto" implementations, currently we just do the latter, which is why we didn't catch this.

We could follow-up and CI the OpenSSL one too, but that was a larger change, so I punted on it.

In any case,
https://lore.kernel.org/git/[email protected]/
in that series un-breaks master, maybe you're interested in peeling it off & fast-tracking it? I didn't submit it separately because I don't know how much of a rush we're in there (given that CI isn't broken).

#49 lore raw
In reply to Glen Choo
Quoted 21 lines · Glen Choo

Cc-ed Johannes, who would know a lot more about CI than I do.

Quoted 10 lines · Junio C Hamano
Quoted 1 line · Glen Choo

At $DAYJOB, we observed that this topic breaks MacOS builds with sha1dc:

Thanks for a report.

How dissapointing. The thing is that the topic has been in 'next' since the 11th (i.e. early last week), and I know that you guys rely on the tip of 'next' in working order to cut your internal releases, but we did not hear about this until now.

Yes. Unfortunately, we (Google's Git team) release on a weekly cadence; we merge on Fridays and build on Mondays (PST), which makes this timing extremely unfortunate.

Quoted 2 lines

Possible action items:

Quoted 7 lines
Quoted 3 lines

* See what configurations our two macOS jobs are using. If neither is using sha1dc, I would say that is criminal [*] and at least one of them should be updated to do so right away.

I'm not too familiar with the CI, but I took a quick peek at ci/lib.sh and noticed that none of the jobs build with sha1dc, not even the Linux or Windows ones, so..

All of our jobs except the OSX one build with SHA1_DC, because it's the default.

Per my just-sent
https://lore.kernel.org/git/[email protected]/
the blind spot has been lack fo SHA1_DC on OSX, for others it's the reverse, we don't test e.g. BLK_SHA1.

In practice we've been catching SHA-implementation specific code early because the OSX implementation was different, but in this case it's OSX-only code, so it only supported the Apple Common Crypto backend.

Quoted 10 lines
Quoted 7 lines

* The "two macOS CI jobs sharing too many configuration knobs" may not be limited to just SHA-1 implementation, but unlike Linux builds and tests, we may have similar "monoculture" issue in our macOS CI builds. Those users, who depend on macOS port being healthy, should help identify unnecessary overlaps between the two, and more importantly, make sure we have CI builds with configuration similar to what they actually use.

I don't think this is a macOS-specific issue; our CI just doesn't do a good job with testing various build configurations.

Yes, definitely.

Quoted 5 lines
Quoted 1 line

* Adding a few build-only-without-tests CI jobs also might help.

This sounds like the way to go IMO. It might be too expensive to run the full test suite on every build configuration, but build-without-test might be ok.

Yes, there's not much point in running the full tests for some of these variants.

A lot of it we can also get for free, e.g. we run some linux jobs with clang, some with gcc etc., we could also run one with BLK_SHA1, one with OPENSSL_SHA1 etc.

#50 lore raw
Quoted 8 lines · Ævar Arnfjörð Bjarmason
Quoted 3 lines · Glen Choo

I'm not too familiar with the CI, but I took a quick peek at ci/lib.sh and noticed that none of the jobs build with sha1dc, not even the Linux or Windows ones, so..

All of our jobs except the OSX one build with SHA1_DC, because it's the default.

Quoted 8 lines

Per my just-sent
https://lore.kernel.org/git/[email protected]/
the blind spot has been lack fo SHA1_DC on OSX, for others it's the reverse, we don't test e.g. BLK_SHA1.

In practice we've been catching SHA-implementation specific code early because the OSX implementation was different, but in this case it's OSX-only code, so it only supported the Apple Common Crypto backend.

I don't know how germane it is to the current thread, but previous discussions[1,2,3,4] favored dropping use of Apple's Common Crypto altogether since it doesn't seem to buy us much (or anything) and is incomplete; it doesn't support all of the OpenSSL API Git uses.

[1]: https://lore.kernel.org/git/CAPig+cTfMx_kwUAxBRHp6kNSOtXsdsv=odUQSRYVpV21DnRuvA@mail.gmail.com/
[2]: https://lore.kernel.org/git/CAMYxyaVQyVRQb-b0nVv412tMZ3rEnOfUPRakg2dEREg5_Ba5Ag@mail.gmail.com/T/
[3]: https://lore.kernel.org/git/[email protected]/
[4]: https://lore.kernel.org/git/CAPig+cQ5kKAt2_RQnqT7Rn=uGmHV9VvxpQ+UgDPOj=D=pq6arg@mail.gmail.com/

#55 lore raw
In reply to Ævar Arnfjörð Bjarmason
Quoted 16 lines · Ævar Arnfjörð Bjarmason
Quoted 6 lines · Junio C Hamano
Quoted 1 line · Glen Choo

At $DAYJOB, we observed that this topic breaks MacOS builds with sha1dc:

Thanks for a report.

Quoted 7 lines

How dissapointing. The thing is that the topic has been in 'next' since the 11th (i.e. early last week), and I know that you guys rely on the tip of 'next' in working order to cut your internal releases, but we did not hear about this until now. What makes it taste even worse is that nobody else caught this, even though we seem to have a couple of macOS jobs at GitHub Actions CI, there we didn't see any breakage related to this.

Quoted 3 lines

FWIW I see you caught it on the 9th in
https://lore.kernel.org/git/[email protected]/, but then the base topic was merged down on the 17th.

Heh, I already forgot I made that comment myself ;-) Thanks for reminding, and thanks for picking the single step to fast-track the fix.

#51 lore raw
In reply to Eric Sunshine
Quoted 4 lines · Eric Sunshine

I don't know how germane it is to the current thread, but previous discussions[1,2,3,4] favored dropping use of Apple's Common Crypto altogether since it doesn't seem to buy us much (or anything) and is incomplete; it doesn't support all of the OpenSSL API Git uses.

Yeah, that matches my recollection. Unless the situation has much changed, dropping it may not be a bad thing to do.

But

* Fixing the fsmonitor code so that it can also be used with things other than Common Crypto is the most urgent. The topic gave us a grave regression (those who used to successfully build Git can no longer build with their favourite configuration).

* Updating the build procedure so that sha1dc is used by default everywhere is a good idea, but that is less urgent and should be done separately, preferrably long after the dust settles from the above.

* Removing Common Crypto support may not be a bad idea, but that is even less urgent, unless the support burden is slowing us down or forcing us to settle on common set of features that is too limiting.

Thanks.

#52 lore raw
In reply to Eric Sunshine
Quoted 24 lines · Eric Sunshine
Quoted 8 lines · Ævar Arnfjörð Bjarmason
Quoted 3 lines · Glen Choo

I'm not too familiar with the CI, but I took a quick peek at ci/lib.sh and noticed that none of the jobs build with sha1dc, not even the Linux or Windows ones, so..

All of our jobs except the OSX one build with SHA1_DC, because it's the default.

Quoted 8 lines

Per my just-sent
https://lore.kernel.org/git/[email protected]/
the blind spot has been lack fo SHA1_DC on OSX, for others it's the reverse, we don't test e.g. BLK_SHA1.

In practice we've been catching SHA-implementation specific code early because the OSX implementation was different, but in this case it's OSX-only code, so it only supported the Apple Common Crypto backend.

I don't know how germane it is to the current thread, but previous discussions[1,2,3,4] favored dropping use of Apple's Common Crypto altogether since it doesn't seem to buy us much (or anything) and is incomplete; it doesn't support all of the OpenSSL API Git uses.

Quoted 4 lines

[1]: https://lore.kernel.org/git/CAPig+cTfMx_kwUAxBRHp6kNSOtXsdsv=odUQSRYVpV21DnRuvA@mail.gmail.com/
[2]: https://lore.kernel.org/git/CAMYxyaVQyVRQb-b0nVv412tMZ3rEnOfUPRakg2dEREg5_Ba5Ag@mail.gmail.com/T/
[3]: https://lore.kernel.org/git/[email protected]/
[4]: https://lore.kernel.org/git/CAPig+cQ5kKAt2_RQnqT7Rn=uGmHV9VvxpQ+UgDPOj=D=pq6arg@mail.gmail.com/

Yeah, maybe. I'd be 100% OK with that happening, but I don't use OSX outside fo CI really, so I wanted to avoid scope creep to "non-SHA stuff we use OpenSSL/AppleCommonCrypto/whatever" for, in the cases where we can/do use OpenSSL/AppleCommonCrypto/whatever for SHA also.

I.e. that's basically a question about what https://, git-imap-send etc. should be using if not vanilla OpenSSL & the like.

But if you/someone can come up with a patch that confidently asserts that it's useless & drops it I'd be happy to either integrate it & submit it as part of this series if it makes things simpler, or alternatively re-roll on top of it.

I'm just not confident in making that case myself, since I hardly use the platform, am not too familiar with the various concerns (aside from skimming the links above), and don't really have interest in pursuing that.

OTOH if you do want to make the case for dropping Apple Common Crypto that would also presumably be much easier after this series, once we're past justifying the hurdle of "wait, we're switching the thing we use for SHA-1 by default, which we build practically everything in git on?" :)

1. https://lore.kernel.org/git/CAPig+cQ5kKAt2_RQnqT7Rn=uGmHV9VvxpQ+UgDPOj=D=pq6arg@mail.gmail.com/

#53 lore raw
Quoted 12 lines · Ævar Arnfjörð Bjarmason
Quoted 10 lines · Eric Sunshine
Quoted 3 lines · Ævar Arnfjörð Bjarmason

In practice we've been catching SHA-implementation specific code early because the OSX implementation was different, but in this case it's OSX-only code, so it only supported the Apple Common Crypto backend.

I don't know how germane it is to the current thread, but previous discussions[1,2,3,4] favored dropping use of Apple's Common Crypto altogether since it doesn't seem to buy us much (or anything) and is incomplete; it doesn't support all of the OpenSSL API Git uses.

Quoted 4 lines

Yeah, maybe. I'd be 100% OK with that happening, but I don't use OSX outside fo CI really, so I wanted to avoid scope creep to "non-SHA stuff we use OpenSSL/AppleCommonCrypto/whatever" for, in the cases where we can/do use OpenSSL/AppleCommonCrypto/whatever for SHA also.

Indeed, I was not suggesting that retirement of AppleCommonCrypto be tackled by you or by the series you posted to fix the build failure; doing so is well outside the scope of the immediate problem. I brought up those old discussions only as reference and as a pointer that whatever fix is eventually adopted for the immediate build problem need not necessarily bend over backward for AppleCommonCrypto support if a long-term goal is to drop AppleCommonCrypto (i.e. do as little as necessary to keep AppleCommonCrypto in a working state, but don't go overboard trying to give it first-class support since it will never be a complete replacement for OpenSSL).

Quoted 9 lines

But if you/someone can come up with a patch that confidently asserts that it's useless & drops it I'd be happy to either integrate it & submit it as part of this series if it makes things simpler, or alternatively re-roll on top of it.

I'm just not confident in making that case myself, since I hardly use the platform, am not too familiar with the various concerns (aside from skimming the links above), and don't really have interest in pursuing that.

I was not suggesting holding up your series or integrating retirement of AppleCommonCrypto with it; they are separate concerns.

Quoted 4 lines

OTOH if you do want to make the case for dropping Apple Common Crypto that would also presumably be much easier after this series, once we're past justifying the hurdle of "wait, we're switching the thing we use for SHA-1 by default, which we build practically everything in git on?"

My "don't know how germane it is to the current thread" observation was in response to the fsmonitor thread, before I ever saw the series you posted for fixing the build failure, so it wasn't given as any sort of feedback on your series, and wasn't asking you to incorporate retirement of AppleCommonCrypto into your series. That said, it does appear that dropping AppleCommonCrypto should become easier after your changes land if someone opts to tackle the task.

#60 lore raw
In reply to Glen Choo
Quoted 3 lines · Glen Choo

At $DAYJOB, we observed that this topic breaks MacOS builds with sha1dc:

  $ make NO_APPLE_COMMON_CRYPTO=1 DC_SHA1=1 NO_OPENSSL=1 compat/fsmonitor/fsm-ipc-darwin.o  

Glen, Ævar has cherry-picked the SHA_CTX -> git_SHA_CTX build fix and I have merged it to 'next'. Can you test-build to see if that change is sufficient to fix "does not even build from the source" issue for you guys? You do not have to go through any official full qualification cycle or release procedure---just checking that you would be OK when you need to do the build the next time before it has to happen is sufficient.

Thanks.

#56 lore raw
In reply to Junio C Hamano
Quoted 5 lines · Junio C Hamano

Possible action items:

* See what configurations our two macOS jobs are using. If neither is using sha1dc, I would say that is criminal [*] and at least one of them should be updated to do so right away.

So here is my "panda-see-panda-do" attempt.

----- >8 --------- >8 --------- >8 --------- >8 --------- >8 -----
Subject: [PATCH] ci: use DC_SHA1=1 on osx-clang job for CI

All other jobs were using the default DC_SHA1 (which is a recommended practice), but the default for macOS jobs being Apple's common crypt, we didn't catch recent breakage soon enough.

We may want to give similar diversity for Linux jobs so that some of them build with other implementations of SHA-1, but let's start small and fill only the immediate need.

---
 ci/lib.sh | 2 ++
 1 file changed, 2 insertions(+)
ci/lib.sh +2 -0
index 1b0cc2b57d..5a115704cb 100755
@@ -259,6 +259,8 @@ macos-latest)
 		MAKEFLAGS="$MAKEFLAGS PYTHON_PATH=$(which python3)"
 	else
 		MAKEFLAGS="$MAKEFLAGS PYTHON_PATH=$(which python2)"
+		MAKEFLAGS="$MAKEFLAGS NO_APPLE_COMMON_CRYPTO=NoThanks"
+		MAKEFLAGS="$MAKEFLAGS DC_SHA1=1 NO_OPENSSL=1"
 	fi
 	;;
 esac
Signature · 1 line
2.38.1-267-g82836053dd
#57 lore raw
Quoted 8 lines · Junio C Hamano

So here is my "panda-see-panda-do" attempt.

----- >8 --------- >8 --------- >8 --------- >8 --------- >8 -----
Subject: [PATCH] ci: use DC_SHA1=1 on osx-clang job for CI

All other jobs were using the default DC_SHA1 (which is a recommended practice), but the default for macOS jobs being Apple's common crypt, we didn't catch recent breakage soon enough.

"recent breakage" is quite vague and probably won't help future readers understand what this is actually fixing. Possible
improvements: (1) a prose description of the breakage; (2) the actual compiler error message; (3) a pointer[1] to the email reporting the problem. One or more of the above improvements to the commit message would help future readers.

[1]: https://lore.kernel.org/git/[email protected]/

Quoted 19 lines

We may want to give similar diversity for Linux jobs so that some of them build with other implementations of SHA-1, but let's start small and fill only the immediate need.

---

ci/lib.sh +2 -0
index 1b0cc2b57d..5a115704cb 100755
@@ -259,6 +259,8 @@ macos-latest)
                MAKEFLAGS="$MAKEFLAGS PYTHON_PATH=$(which python3)"
        else
                MAKEFLAGS="$MAKEFLAGS PYTHON_PATH=$(which python2)"
+               MAKEFLAGS="$MAKEFLAGS NO_APPLE_COMMON_CRYPTO=NoThanks"
+               MAKEFLAGS="$MAKEFLAGS DC_SHA1=1 NO_OPENSSL=1"
        fi
        ;;
 esac
#58 lore raw
Quoted 10 lines · Eric Sunshine
Quoted 8 lines · Junio C Hamano

So here is my "panda-see-panda-do" attempt.

----- >8 --------- >8 --------- >8 --------- >8 --------- >8 -----
Subject: [PATCH] ci: use DC_SHA1=1 on osx-clang job for CI

All other jobs were using the default DC_SHA1 (which is a recommended practice), but the default for macOS jobs being Apple's common crypt, we didn't catch recent breakage soon enough.

Quoted 6 lines

"recent breakage" is quite vague and probably won't help future readers understand what this is actually fixing. Possible
improvements: (1) a prose description of the breakage; (2) the actual compiler error message; (3) a pointer[1] to the email reporting the problem. One or more of the above improvements to the commit message would help future readers.

I do not think (2) or (3) would help all that much. A finger that points at the exact commit that broke the build (with the condition under which the build breaks) would probably be the most useful to those who read "git log" output.

Thanks.

----- >8 --------- >8 --------- >8 --------- >8 --------- >8 -----
Subject: [PATCH] ci: use DC_SHA1=YesPlease on osx-clang job for CI

7b8cfe34 (Merge branch 'ed/fsmonitor-on-networked-macos', 2022-10-17) broke the build on macOS with sha1dc by bypassing our hash abstraction (git_SHA_CTX etc.), but it wasn't caught before the problematic topic was merged down to the 'master' branch. Nobody was even compile testing with DC_SHA1 set, although it is the recommended choice in these days for folks when they use SHA-1.

This was because the default for macOS uses Apple Common Crypto, and both of the two CI jobs did not override the default. Tweak one of them to use DC_SHA1 to improve the coverage.

We may want to give similar diversity for Linux jobs so that some of them build with other implementations of SHA-1; they currently all build and test with DC_SHA1 as that is the default on everywhere other than macOS.

But let's start small to fill only the immediate need.

---
 ci/lib.sh | 2 ++
 1 file changed, 2 insertions(+)
ci/lib.sh +2 -0
index 1b0cc2b57d..51e47aa5d6 100755
@@ -259,6 +259,8 @@ macos-latest)
 		MAKEFLAGS="$MAKEFLAGS PYTHON_PATH=$(which python3)"
 	else
 		MAKEFLAGS="$MAKEFLAGS PYTHON_PATH=$(which python2)"
+		MAKEFLAGS="$MAKEFLAGS NO_APPLE_COMMON_CRYPTO=NoThanks"
+		MAKEFLAGS="$MAKEFLAGS DC_SHA1=YesPlease NO_OPENSSL=NoThanks"
 	fi
 	;;
 esac
Signature · 1 line
2.38.1-271-ge78cd6c470
#59 lore raw
Quoted 28 lines · Junio C Hamano
Quoted 11 lines · Eric Sunshine
Quoted 3 lines · Junio C Hamano

All other jobs were using the default DC_SHA1 (which is a recommended practice), but the default for macOS jobs being Apple's common crypt, we didn't catch recent breakage soon enough.

"recent breakage" is quite vague and probably won't help future readers understand what this is actually fixing. Possible
improvements: (1) a prose description of the breakage; (2) the actual compiler error message; (3) a pointer[1] to the email reporting the problem. One or more of the above improvements to the commit message would help future readers.

I do not think (2) or (3) would help all that much. A finger that points at the exact commit that broke the build (with the condition under which the build breaks) would probably be the most useful to those who read "git log" output.

----- >8 --------- >8 --------- >8 --------- >8 --------- >8 -----
Subject: [PATCH] ci: use DC_SHA1=YesPlease on osx-clang job for CI

7b8cfe34 (Merge branch 'ed/fsmonitor-on-networked-macos', 2022-10-17) broke the build on macOS with sha1dc by bypassing our hash abstraction (git_SHA_CTX etc.), but it wasn't caught before the problematic topic was merged down to the 'master' branch. Nobody was even compile testing with DC_SHA1 set, although it is the recommended choice in these days for folks when they use SHA-1.

Quoted 3 lines

This was because the default for macOS uses Apple Common Crypto, and both of the two CI jobs did not override the default. Tweak one of them to use DC_SHA1 to improve the coverage.

Thanks. This revised commit message does a much better job of explaining the problem the patch is addressing.

#61 lore raw
In reply to Junio C Hamano
Quoted 14 lines · Junio C Hamano
Quoted 3 lines · Glen Choo

At $DAYJOB, we observed that this topic breaks MacOS builds with sha1dc:

  $ make NO_APPLE_COMMON_CRYPTO=1 DC_SHA1=1 NO_OPENSSL=1 compat/fsmonitor/fsm-ipc-darwin.o  

Glen, Ævar has cherry-picked the SHA_CTX -> git_SHA_CTX build fix and I have merged it to 'next'. Can you test-build to see if that change is sufficient to fix "does not even build from the source" issue for you guys? You do not have to go through any official full qualification cycle or release procedure---just checking that you would be OK when you need to do the build the next time before it has to happen is sufficient.

Quoted 1 line

Thanks.

FYI, https://github.com/git/git/actions/runs/3292710187 is a successful (wow, what's a rare event these days) run of CI on 'seen' that includes

* Peff's -O0 fix for unconfusing LSan to prevent it from giving false positives

* Eric DeCosta's change, cherry-picked by Ævar, to let fsmonitor stuff compile with sha1dc as well as Apple's Common Crypto.

* A tweak to make one of the macOS CI jobs to build with sha1dc.

among all the other topics in flight. I plan to merge all of them to 'next' (I think the first two are already in 'next') and fast track them down to 'master'.

#62 lore raw

v3 cover fsmonitor: Implement fsmonitor for Linux

In reply to Eric DeCosta via GitGitGadget

Goal is to deliver fsmonitor for Linux that is on par with fsmonitor for Windows and Mac OS.

This patch set builds upon previous work for done for Windows and Mac OS to implement a fsmonitor back-end for Linux based on the Linux inotify API. inotify differs significantly from the equivalent Windows and Mac OS APIs in that a watch must be registered for every directory of interest (rather than a singular watch at the root of the directory tree) and special care must be taken to handle directory renames correctly.

More information about inotify:
https://man7.org/linux/man-pages/man7/inotify.7.html

v2 differs from v1:

* Prior work for Windows and Mac OS has been merged to master, reducing the patch set from 12 to 6 patches
* Code review feedback
* Identified and resolved race condition revealed by CI test system, see "Limitations and caveats" regarding monitoring of directory trees from the man page, above
* Apologies for being away from this for so long, but my attention was needed elsewhere

v1 differs from v0:

* Code review feedback
* Update how and which code can be shared between Mac OS and Linux
* Increase polling frequency to every 1ms (matches Mac OS)
* Updates to t7527 to improve test stability

Eric DeCosta (6):
fsmonitor: prepare to share code between Mac OS and Linux
fsmonitor: determine if filesystem is local or remote
fsmonitor: implement filesystem change listener for Linux
fsmonitor: enable fsmonitor for Linux
fsmonitor: test updates
fsmonitor: update doc for Linux

 Documentation/config/fsmonitor--daemon.txt |   4 +-
 Documentation/git-fsmonitor--daemon.txt    |  24 +-
 compat/fsmonitor/fsm-health-linux.c        |  24 +
 compat/fsmonitor/fsm-ipc-darwin.c          |  53 +-
 compat/fsmonitor/fsm-ipc-linux.c           |   1 +
 compat/fsmonitor/fsm-ipc-unix.c            |  52 ++
 compat/fsmonitor/fsm-listen-linux.c        | 676 +++++++++++++++++++++
 compat/fsmonitor/fsm-path-utils-linux.c    | 169 ++++++
 compat/fsmonitor/fsm-settings-darwin.c     |  63 +-
 compat/fsmonitor/fsm-settings-linux.c      |   1 +
 compat/fsmonitor/fsm-settings-unix.c       |  61 ++
 config.mak.uname                           |   8 +
 contrib/buildsystems/CMakeLists.txt        |  11 +-
 t/t7527-builtin-fsmonitor.sh               |  94 ++-
 14 files changed, 1102 insertions(+), 139 deletions(-)
 create mode 100644 compat/fsmonitor/fsm-health-linux.c
 create mode 100644 compat/fsmonitor/fsm-ipc-linux.c
 create mode 100644 compat/fsmonitor/fsm-ipc-unix.c
 create mode 100644 compat/fsmonitor/fsm-listen-linux.c
 create mode 100644 compat/fsmonitor/fsm-path-utils-linux.c
 create mode 100644 compat/fsmonitor/fsm-settings-linux.c
 create mode 100644 compat/fsmonitor/fsm-settings-unix.c

base-commit: 319605f8f00e402f3ea758a02c63534ff800a711
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-1352%2Fedecosta-mw%2Ffsmonitor_linux-v3
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-1352/edecosta-mw/fsmonitor_linux-v3
Pull-Request: https://github.com/git/git/pull/1352

Range-diff vs v2:

  1:  cd46bed37c3 <  -:  ----------- fsmonitor: refactor filesystem checks to common interface
  2:  21d114bda4b <  -:  ----------- fsmonitor: relocate socket file if .git directory is remote
  3:  664259ed57a <  -:  ----------- fsmonitor: avoid socket location check if using hook
  4:  d8f20032d6b <  -:  ----------- fsmonitor: deal with synthetic firmlinks on macOS
  5:  ab1e0ab967c <  -:  ----------- fsmonitor: check for compatability before communicating with fsmonitor
  6:  9c552239b57 <  -:  ----------- fsmonitor: add documentation for allowRemote and socketDir options
  7:  295beb89ab1 !  1:  99d684c7bdf fsmonitor: prepare to share code between Mac OS and Linux
     @@ Commit message
      
          Linux and Mac OS can share some of the code originally developed for Mac OS.
      
     -    Minor update to compat/fsmonitor/fsm-ipc-unix.c to make it cross-platform.
     -    Mac OS and Linux can share fsm-ipc-unix.c
     -
     -    Both platforms can also share compat/fsmonitor/fsm-settings-unix.c but we
     -    will leave room for future, platform-specific checks by having the platform-
     -    specific implementations call into fsm-settings-unix.
     +    Mac OS and Linux can share fsm-ipc-unix.c and fsm-settings-unix.c
      
          Signed-off-by: Eric DeCosta <[email protected]>
      
     - ## Makefile ##
     -@@ Makefile: endif
     - 
     - ifdef FSMONITOR_DAEMON_BACKEND
     - 	COMPAT_CFLAGS += -DHAVE_FSMONITOR_DAEMON_BACKEND
     -+	ifdef FSMONITOR_DAEMON_COMMON
     -+		COMPAT_OBJS += compat/fsmonitor/fsm-ipc-$(FSMONITOR_DAEMON_COMMON).o
     -+	else
     -+		COMPAT_OBJS += compat/fsmonitor/fsm-ipc-$(FSMONITOR_DAEMON_BACKEND).o
     -+	endif
     - 	COMPAT_OBJS += compat/fsmonitor/fsm-listen-$(FSMONITOR_DAEMON_BACKEND).o
     - 	COMPAT_OBJS += compat/fsmonitor/fsm-health-$(FSMONITOR_DAEMON_BACKEND).o
     --	COMPAT_OBJS += compat/fsmonitor/fsm-ipc-$(FSMONITOR_DAEMON_BACKEND).o
     - endif
     - 
     - ifdef FSMONITOR_OS_SETTINGS
     - 	COMPAT_CFLAGS += -DHAVE_FSMONITOR_OS_SETTINGS
     -+	ifdef FSMONITOR_DAEMON_COMMON
     -+		COMPAT_OBJS += compat/fsmonitor/fsm-settings-$(FSMONITOR_DAEMON_COMMON).o
     -+	endif
     - 	COMPAT_OBJS += compat/fsmonitor/fsm-settings-$(FSMONITOR_OS_SETTINGS).o
     - 	COMPAT_OBJS += compat/fsmonitor/fsm-path-utils-$(FSMONITOR_OS_SETTINGS).o
     - endif
     -
       ## compat/fsmonitor/fsm-health-linux.c (new) ##
      @@
      +#include "cache.h"
     @@ compat/fsmonitor/fsm-health-linux.c (new)
      +{
      +}
      
     - ## compat/fsmonitor/fsm-ipc-darwin.c => compat/fsmonitor/fsm-ipc-unix.c ##
     -@@ compat/fsmonitor/fsm-ipc-unix.c: static GIT_PATH_FUNC(fsmonitor_ipc__get_default_path, "fsmonitor--daemon.ipc")
     - const char *fsmonitor_ipc__get_path(struct repository *r)
     - {
     - 	static const char *ipc_path = NULL;
     --	SHA_CTX sha1ctx;
     + ## compat/fsmonitor/fsm-ipc-darwin.c ##
     +@@
     +-#include "cache.h"
     +-#include "config.h"
     +-#include "strbuf.h"
     +-#include "fsmonitor.h"
     +-#include "fsmonitor-ipc.h"
     +-#include "fsmonitor-path-utils.h"
     +-
     +-static GIT_PATH_FUNC(fsmonitor_ipc__get_default_path, "fsmonitor--daemon.ipc")
     +-
     +-const char *fsmonitor_ipc__get_path(struct repository *r)
     +-{
     +-	static const char *ipc_path = NULL;
     +-	git_SHA_CTX sha1ctx;
     +-	char *sock_dir = NULL;
     +-	struct strbuf ipc_file = STRBUF_INIT;
     +-	unsigned char hash[GIT_MAX_RAWSZ];
     +-
     +-	if (!r)
     +-		BUG("No repository passed into fsmonitor_ipc__get_path");
     +-
     +-	if (ipc_path)
     +-		return ipc_path;
     +-
     +-
     +-	/* By default the socket file is created in the .git directory */
     +-	if (fsmonitor__is_fs_remote(r->gitdir) < 1) {
     +-		ipc_path = fsmonitor_ipc__get_default_path();
     +-		return ipc_path;
     +-	}
     +-
     +-	git_SHA1_Init(&sha1ctx);
     +-	git_SHA1_Update(&sha1ctx, r->worktree, strlen(r->worktree));
     +-	git_SHA1_Final(hash, &sha1ctx);
     +-
     +-	repo_config_get_string(r, "fsmonitor.socketdir", &sock_dir);
     +-
     +-	/* Create the socket file in either socketDir or $HOME */
     +-	if (sock_dir && *sock_dir) {
     +-		strbuf_addf(&ipc_file, "%s/.git-fsmonitor-%s",
     +-					sock_dir, hash_to_hex(hash));
     +-	} else {
     +-		strbuf_addf(&ipc_file, "~/.git-fsmonitor-%s", hash_to_hex(hash));
     +-	}
     +-	free(sock_dir);
     +-
     +-	ipc_path = interpolate_path(ipc_file.buf, 1);
     +-	if (!ipc_path)
     +-		die(_("Invalid path: %s"), ipc_file.buf);
     +-
     +-	strbuf_release(&ipc_file);
     +-	return ipc_path;
     +-}
     ++#include "fsm-ipc-unix.c"
     +
     + ## compat/fsmonitor/fsm-ipc-linux.c (new) ##
     +@@
     ++#include "fsm-ipc-unix.c"
     +
     + ## compat/fsmonitor/fsm-ipc-unix.c (new) ##
     +@@
     ++#include "cache.h"
     ++#include "config.h"
     ++#include "strbuf.h"
     ++#include "fsmonitor.h"
     ++#include "fsmonitor-ipc.h"
     ++#include "fsmonitor-path-utils.h"
     ++
     ++static GIT_PATH_FUNC(fsmonitor_ipc__get_default_path, "fsmonitor--daemon.ipc")
     ++
     ++const char *fsmonitor_ipc__get_path(struct repository *r)
     ++{
     ++	static const char *ipc_path = NULL;
      +	git_SHA_CTX sha1ctx;
     - 	char *sock_dir = NULL;
     - 	struct strbuf ipc_file = STRBUF_INIT;
     - 	unsigned char hash[SHA_DIGEST_LENGTH];
     -@@ compat/fsmonitor/fsm-ipc-unix.c: const char *fsmonitor_ipc__get_path(struct repository *r)
     - 		return ipc_path;
     - 	}
     - 
     --	SHA1_Init(&sha1ctx);
     --	SHA1_Update(&sha1ctx, r->worktree, strlen(r->worktree));
     --	SHA1_Final(hash, &sha1ctx);
     ++	char *sock_dir = NULL;
     ++	struct strbuf ipc_file = STRBUF_INIT;
     ++	unsigned char hash[GIT_MAX_RAWSZ];
     ++
     ++	if (!r)
     ++		BUG("No repository passed into fsmonitor_ipc__get_path");
     ++
     ++	if (ipc_path)
     ++		return ipc_path;
     ++
     ++
     ++	/* By default the socket file is created in the .git directory */
     ++	if (fsmonitor__is_fs_remote(r->gitdir) < 1) {
     ++		ipc_path = fsmonitor_ipc__get_default_path();
     ++		return ipc_path;
     ++	}
     ++
      +	git_SHA1_Init(&sha1ctx);
      +	git_SHA1_Update(&sha1ctx, r->worktree, strlen(r->worktree));
      +	git_SHA1_Final(hash, &sha1ctx);
     - 
     - 	repo_config_get_string(r, "fsmonitor.socketdir", &sock_dir);
     - 
     ++
     ++	repo_config_get_string(r, "fsmonitor.socketdir", &sock_dir);
     ++
     ++	/* Create the socket file in either socketDir or $HOME */
     ++	if (sock_dir && *sock_dir) {
     ++		strbuf_addf(&ipc_file, "%s/.git-fsmonitor-%s",
     ++					sock_dir, hash_to_hex(hash));
     ++	} else {
     ++		strbuf_addf(&ipc_file, "~/.git-fsmonitor-%s", hash_to_hex(hash));
     ++	}
     ++	free(sock_dir);
     ++
     ++	ipc_path = interpolate_path(ipc_file.buf, 1);
     ++	if (!ipc_path)
     ++		die(_("Invalid path: %s"), ipc_file.buf);
     ++
     ++	strbuf_release(&ipc_file);
     ++	return ipc_path;
     ++}
      
       ## compat/fsmonitor/fsm-settings-darwin.c ##
      @@
     - #include "config.h"
     - #include "fsmonitor.h"
     - #include "fsmonitor-ipc.h"
     +-#include "config.h"
     +-#include "fsmonitor.h"
     +-#include "fsmonitor-ipc.h"
      -#include "fsmonitor-settings.h"
     - #include "fsmonitor-path-utils.h"
     +-#include "fsmonitor-path-utils.h"
      -
      - /*
      - * For the builtin FSMonitor, we create the Unix domain socket for the
     @@ compat/fsmonitor/fsm-settings-darwin.c
      -	free(fs.typename);
      -	return FSMONITOR_REASON_OK;
      -}
     -+#include "fsmonitor-settings.h"
     -+#include "fsm-settings-unix.h"
     - 
     - enum fsmonitor_reason fsm_os__incompatible(struct repository *r, int ipc)
     - {
     +-
     +-enum fsmonitor_reason fsm_os__incompatible(struct repository *r, int ipc)
     +-{
      -	enum fsmonitor_reason reason;
      -
      -	if (ipc) {
     @@ compat/fsmonitor/fsm-settings-darwin.c
      -	}
      -
      -	return FSMONITOR_REASON_OK;
     -+    return fsm_os__incompatible_unix(r, ipc);
     - }
     +-}
     ++#include "fsm-settings-unix.c"
      
       ## compat/fsmonitor/fsm-settings-linux.c (new) ##
      @@
     -+#include "config.h"
     -+#include "fsmonitor.h"
     -+#include "fsmonitor-ipc.h"
     -+#include "fsmonitor-path-utils.h"
     -+#include "fsmonitor-settings.h"
     -+#include "fsm-settings-unix.h"
     -+
     -+enum fsmonitor_reason fsm_os__incompatible(struct repository *r, int ipc)
     -+{
     -+    return fsm_os__incompatible_unix(r, ipc);
     -+}
     ++#include "fsm-settings-unix.c"
      
       ## compat/fsmonitor/fsm-settings-unix.c (new) ##
      @@
     @@ compat/fsmonitor/fsm-settings-unix.c (new)
      +#include "fsmonitor.h"
      +#include "fsmonitor-ipc.h"
      +#include "fsmonitor-path-utils.h"
     -+#include "fsm-settings-unix.h"
      +
      + /*
      + * For the builtin FSMonitor, we create the Unix domain socket for the
     @@ compat/fsmonitor/fsm-settings-unix.c (new)
      +	struct fs_info fs;
      +	const char *ipc_path = fsmonitor_ipc__get_path(r);
      +	struct strbuf path = STRBUF_INIT;
     -+	strbuf_add(&path, ipc_path, strlen(ipc_path));
     ++	strbuf_addstr(&path, ipc_path);
      +
      +	if (fsmonitor__get_fs_info(dirname(path.buf), &fs) == -1) {
      +		strbuf_release(&path);
     @@ compat/fsmonitor/fsm-settings-unix.c (new)
      +	return FSMONITOR_REASON_OK;
      +}
      +
     -+enum fsmonitor_reason fsm_os__incompatible_unix(struct repository *r, int ipc)
     ++enum fsmonitor_reason fsm_os__incompatible(struct repository *r, int ipc)
      +{
      +	enum fsmonitor_reason reason;
      +
     @@ compat/fsmonitor/fsm-settings-unix.c (new)
      +
      +	return FSMONITOR_REASON_OK;
      +}
     -
     - ## compat/fsmonitor/fsm-settings-unix.h (new) ##
     -@@
     -+#ifndef FSM_SETTINGS_UNIX_H
     -+#define FSM_SETTINGS_UNIX_H
     -+
     -+#ifdef HAVE_FSMONITOR_OS_SETTINGS
     -+/*
     -+ * Check for compatibility on unix-like systems (e.g. Darwin and Linux)
     -+ */
     -+enum fsmonitor_reason fsm_os__incompatible_unix(struct repository *r, int ipc);
     -+#endif /* HAVE_FSMONITOR_OS_SETTINGS */
     -+
     -+#endif /* FSM_SETTINGS_UNIX_H */
     -
     - ## config.mak.uname ##
     -@@ config.mak.uname: ifeq ($(uname_S),Darwin)
     - 	ifndef NO_UNIX_SOCKETS
     - 	FSMONITOR_DAEMON_BACKEND = darwin
     - 	FSMONITOR_OS_SETTINGS = darwin
     -+	FSMONITOR_DAEMON_COMMON = unix
     - 	endif
     - 	endif
     - 
     -
     - ## contrib/buildsystems/CMakeLists.txt ##
     -@@ contrib/buildsystems/CMakeLists.txt: else()
     - endif()
     - 
     - if(SUPPORTS_SIMPLE_IPC)
     --	if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
     -+	if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
     -+		add_compile_definitions(HAVE_FSMONITOR_DAEMON_BACKEND)
     -+		list(APPEND compat_SOURCES compat/fsmonitor/fsm-ipc-unix.c)
     -+		list(APPEND compat_SOURCES compat/fsmonitor/fsm-health-linux.c)
     -+		list(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-linux.c)
     -+		list(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-linux.c)
     -+
     -+		add_compile_definitions(HAVE_FSMONITOR_OS_SETTINGS)
     -+		list(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-unix.c)
     -+		list(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-linux.c)
     -+	elseif(CMAKE_SYSTEM_NAME STREQUAL "Windows")
     - 		add_compile_definitions(HAVE_FSMONITOR_DAEMON_BACKEND)
     - 		list(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-win32.c)
     - 		list(APPEND compat_SOURCES compat/fsmonitor/fsm-health-win32.c)
     -@@ contrib/buildsystems/CMakeLists.txt: if(SUPPORTS_SIMPLE_IPC)
     - 		list(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-win32.c)
     - 	elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
     - 		add_compile_definitions(HAVE_FSMONITOR_DAEMON_BACKEND)
     --		list(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-darwin.c)
     -+		list(APPEND compat_SOURCES compat/fsmonitor/fsm-ipc-unix.c)
     - 		list(APPEND compat_SOURCES compat/fsmonitor/fsm-health-darwin.c)
     --		list(APPEND compat_SOURCES compat/fsmonitor/fsm-ipc-darwin.c)
     -+		list(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-darwin.c)
     - 		list(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-darwin.c)
     - 
     - 		add_compile_definitions(HAVE_FSMONITOR_OS_SETTINGS)
     -+		list(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-unix.c)
     - 		list(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-darwin.c)
     - 	endif()
     - endif()
  8:  7d7ef78728f =  2:  aa405379891 fsmonitor: determine if filesystem is local or remote
  9:  4f9c5358475 !  3:  c2e5a7201aa fsmonitor: implement filesystem change listener for Linux
     @@ compat/fsmonitor/fsm-listen-linux.c (new)
      +/*
      + * Recursively add watches to every directory under path
      + */
     -+static int register_inotify(const char *path, struct fsm_listen_data *data)
     ++static int register_inotify(const char *path,
     ++	struct fsmonitor_daemon_state *state,
     ++	struct fsmonitor_batch *batch)
      +{
      +	DIR *dir;
     ++	const char *rel;
      +	struct strbuf current = STRBUF_INIT;
      +	struct dirent *de;
      +	struct stat fs;
     @@ compat/fsmonitor/fsm-listen-linux.c (new)
      +
      +		/* recurse into directory */
      +		if (S_ISDIR(fs.st_mode)) {
     -+			if (add_watch(current.buf, data))
     ++			if (add_watch(current.buf, state->listen_data))
      +				goto failed;
     -+			if (register_inotify(current.buf, data))
     ++			if (register_inotify(current.buf, state, batch))
      +				goto failed;
     ++		} else if (batch) {
     ++			rel = current.buf + state->path_worktree_watch.len + 1;
     ++			trace_printf_key(&trace_fsmonitor, "explicitly adding '%s'", rel);
     ++			fsmonitor_batch__add_path(batch, rel);
      +		}
      +	}
      +	ret = 0;
     @@ compat/fsmonitor/fsm-listen-linux.c (new)
      +
      +	if (add_watch(state->path_worktree_watch.buf, data))
      +		ret = -1;
     -+	else if (register_inotify(state->path_worktree_watch.buf, data))
     ++	else if (register_inotify(state->path_worktree_watch.buf, state, NULL))
      +		ret = -1;
      +	else if (state->nr_paths_watching > 1) {
      +		if (add_watch(state->path_gitdir_watch.buf, data))
      +			ret = -1;
     -+		else if (register_inotify(state->path_gitdir_watch.buf, data))
     ++		else if (register_inotify(state->path_gitdir_watch.buf, state, NULL))
      +			ret = -1;
      +	}
      +
     @@ compat/fsmonitor/fsm-listen-linux.c (new)
      +				add_dir_rename(event->cookie, path, state->listen_data);
      +
      +			/* received IN_MOVE_TO, update watch to reflect new path */
     -+			if (em_rename_dir_to(event->mask))
     ++			if (em_rename_dir_to(event->mask)) {
      +				rename_dir(event->cookie, path, state->listen_data);
     ++				if (register_inotify(path, state, batch)) {
     ++					state->listen_data->shutdown = SHUTDOWN_ERROR;
     ++					goto done;
     ++				}
     ++			}
      +
      +			if (em_dir_created(event->mask)) {
      +				if (add_watch(path, state->listen_data)) {
      +					state->listen_data->shutdown = SHUTDOWN_ERROR;
      +					goto done;
      +				}
     -+				if (register_inotify(path, state->listen_data)) {
     ++				if (register_inotify(path, state, batch)) {
      +					state->listen_data->shutdown = SHUTDOWN_ERROR;
      +					goto done;
      +				}
     @@ compat/fsmonitor/fsm-listen-linux.c (new)
      +			strbuf_reset(&path);
      +			strbuf_add(&path, w->dir, strlen(w->dir));
      +			strbuf_addch(&path, '/');
     -+			strbuf_add(&path, event->name,  strlen(event->name));
     ++			strbuf_addstr(&path, event->name);
      +
      +			p = fsmonitor__resolve_alias(path.buf, &state->alias);
      +			if (!p)
 10:  07650ecd27b !  4:  05f5b2dd4fb fsmonitor: enable fsmonitor for Linux
     @@ config.mak.uname: ifeq ($(uname_S),Linux)
      +	ifndef NO_UNIX_SOCKETS
      +	FSMONITOR_DAEMON_BACKEND = linux
      +	FSMONITOR_OS_SETTINGS = linux
     -+	FSMONITOR_DAEMON_COMMON = unix
      +	endif
      +	endif
       endif
       ifeq ($(uname_S),GNU/kFreeBSD)
       	HAVE_ALLOCA_H = YesPlease
     +
     + ## contrib/buildsystems/CMakeLists.txt ##
     +@@ contrib/buildsystems/CMakeLists.txt: else()
     + endif()
     + 
     + if(SUPPORTS_SIMPLE_IPC)
     +-	if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
     ++	if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
     ++		add_compile_definitions(HAVE_FSMONITOR_DAEMON_BACKEND)
     ++		list(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-linux.c)
     ++		list(APPEND compat_SOURCES compat/fsmonitor/fsm-health-linux.c)
     ++		list(APPEND compat_SOURCES compat/fsmonitor/fsm-ipc-linux.c)
     ++		list(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-linux.c)
     ++
     ++		add_compile_definitions(HAVE_FSMONITOR_OS_SETTINGS)
     ++		list(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-linux.c)
     ++	elseif(CMAKE_SYSTEM_NAME STREQUAL "Windows")
     + 		add_compile_definitions(HAVE_FSMONITOR_DAEMON_BACKEND)
     + 		list(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-win32.c)
     + 		list(APPEND compat_SOURCES compat/fsmonitor/fsm-health-win32.c)
 11:  6682938fff8 !  5:  754355ca44f fsmonitor: test updates
     @@ t/t7527-builtin-fsmonitor.sh: start_daemon () {
      +		then
      +			if test "$last" -eq "$nsz"
      +			then
     ++				cat "$file" &&
      +				return 0
      +			fi
      +			last=$nsz
     @@ t/t7527-builtin-fsmonitor.sh: start_daemon () {
      +		sleep 1
      +		k=$(( $k + 1 ))
      +	done &&
     ++	cat "$file" &&
      +	return 0
      +}
      +
     @@ t/t7527-builtin-fsmonitor.sh: create_files () {
      +rename_directory () {
      +	mv dirtorename dirrenamed
      +}
     ++
     ++rename_directory_file () {
     ++	mv dirtorename dirrenamed &&
     ++	echo 1 > dirrenamed/new
     ++}
      +
       rename_files () {
       	mv rename renamed &&
     @@ t/t7527-builtin-fsmonitor.sh: test_expect_success 'rename directory' '
       	grep "^event: dirtorename/*$" .git/trace &&
       	grep "^event: dirrenamed/*$"  .git/trace
       '
     -@@ t/t7527-builtin-fsmonitor.sh: test_expect_success 'file changes to directory' '
     + 
     ++test_expect_success 'rename directory file' '
     ++	test_when_finished clean_up_repo_and_stop_daemon &&
     ++
     ++	start_daemon --tf "$PWD/.git/trace" &&
     ++
     ++	wait_for_update rename_directory_file "$PWD/.git/trace" &&
     ++
     ++	test-tool fsmonitor-client query --token 0 &&
     ++
     ++	test_might_fail git fsmonitor--daemon stop &&
     ++
     ++	grep "^event: dirtorename/*$" .git/trace &&
     ++	grep "^event: dirrenamed/*$"  .git/trace &&
     ++	grep "^event: dirrenamed/new$"  .git/trace
     ++'
     + test_expect_success 'file changes to directory' '
     + 	test_when_finished clean_up_repo_and_stop_daemon &&
       
       	start_daemon --tf "$PWD/.git/trace" &&
       
     @@ t/t7527-builtin-fsmonitor.sh: u_values="$u1 $u2"
       
       		git init "$u" &&
       		echo 1 >"$u"/file1 &&
     -@@ t/t7527-builtin-fsmonitor.sh: my_match_and_clean () {
     - }
     +@@ t/t7527-builtin-fsmonitor.sh: test_expect_success 'submodule setup' '
     + '
       
       test_expect_success 'submodule always visited' '
      -	test_when_finished "git -C super fsmonitor--daemon stop; \

12: dd73e126810 = 6: f56175e097a fsmonitor: update doc for Linux

Signature · 1 line
gitgitgadget
#63 lore raw

PATCH v3 1/6 fsmonitor: prepare to share code between Mac OS and Linux

From: Eric DeCosta <[email protected]>

Linux and Mac OS can share some of the code originally developed for Mac OS.

Mac OS and Linux can share fsm-ipc-unix.c and fsm-settings-unix.c

---
 compat/fsmonitor/fsm-health-linux.c    | 24 ++++++++++
 compat/fsmonitor/fsm-ipc-darwin.c      | 53 +---------------------
 compat/fsmonitor/fsm-ipc-linux.c       |  1 +
 compat/fsmonitor/fsm-ipc-unix.c        | 52 +++++++++++++++++++++
 compat/fsmonitor/fsm-settings-darwin.c | 63 +-------------------------
 compat/fsmonitor/fsm-settings-linux.c  |  1 +
 compat/fsmonitor/fsm-settings-unix.c   | 61 +++++++++++++++++++++++++
 7 files changed, 141 insertions(+), 114 deletions(-)
 create mode 100644 compat/fsmonitor/fsm-health-linux.c
 create mode 100644 compat/fsmonitor/fsm-ipc-linux.c
 create mode 100644 compat/fsmonitor/fsm-ipc-unix.c
 create mode 100644 compat/fsmonitor/fsm-settings-linux.c
 create mode 100644 compat/fsmonitor/fsm-settings-unix.c
compat/fsmonitor/fsm-health-linux.c new file +24 -0
new file mode 100644
index 00000000000..b9f709e8548
@@ -0,0 +1,24 @@
+#include "cache.h"
+#include "config.h"
+#include "fsmonitor.h"
+#include "fsm-health.h"
+#include "fsmonitor--daemon.h"
+
+int fsm_health__ctor(struct fsmonitor_daemon_state *state)
+{
+	return 0;
+}
+
+void fsm_health__dtor(struct fsmonitor_daemon_state *state)
+{
+	return;
+}
+
+void fsm_health__loop(struct fsmonitor_daemon_state *state)
+{
+	return;
+}
+
+void fsm_health__stop_async(struct fsmonitor_daemon_state *state)
+{
+}
compat/fsmonitor/fsm-ipc-darwin.c +1 -52

53 lines not on this page · raw message

compat/fsmonitor/fsm-ipc-linux.c new file +1 -0
new file mode 100644
index 00000000000..4c3c92081ee
@@ -0,0 +1 @@
+#include "fsm-ipc-unix.c"
compat/fsmonitor/fsm-ipc-unix.c new file +52 -0

52 lines not on this page · raw message

compat/fsmonitor/fsm-settings-darwin.c +1 -62

63 lines not on this page · raw message

compat/fsmonitor/fsm-settings-linux.c new file +1 -0
new file mode 100644
index 00000000000..14baf9f0603
@@ -0,0 +1 @@
+#include "fsm-settings-unix.c"
compat/fsmonitor/fsm-settings-unix.c new file +61 -0

61 lines not on this page · raw message

Signature · 1 line
gitgitgadget
#64 lore raw

PATCH v3 2/6 fsmonitor: determine if filesystem is local or remote

In reply to Eric DeCosta via GitGitGadget

From: Eric DeCosta <[email protected]>

Compare the given path to the mounted filesystems. Find the mount that is the longest prefix of the path (if any) and determine if that mount is on a local or remote filesystem.

---
 compat/fsmonitor/fsm-path-utils-linux.c | 169 ++++++++++++++++++++++++
 1 file changed, 169 insertions(+)
 create mode 100644 compat/fsmonitor/fsm-path-utils-linux.c
compat/fsmonitor/fsm-path-utils-linux.c new file +169 -0
new file mode 100644
index 00000000000..039f044b670
@@ -0,0 +1,169 @@
+#include "fsmonitor.h"
+#include "fsmonitor-path-utils.h"
+#include <errno.h>
+#include <mntent.h>
+#include <sys/mount.h>
+#include <sys/statvfs.h>
+
+/*
+ * https://github.com/coreutils/gnulib/blob/master/lib/mountlist.c
+ */
+#ifndef ME_REMOTE
+/* A file system is "remote" if its Fs_name contains a ':'
+   or if (it is of type (smbfs or cifs) and its Fs_name starts with '//')
+   or if it is of any other of the listed types
+   or Fs_name is equal to "-hosts" (used by autofs to mount remote fs).
+   "VM" file systems like prl_fs or vboxsf are not considered remote here. */
+# define ME_REMOTE(Fs_name, Fs_type)            \
+	(strchr (Fs_name, ':') != NULL              \
+	 || ((Fs_name)[0] == '/'                    \
+		 && (Fs_name)[1] == '/'                 \
+		 && (strcmp (Fs_type, "smbfs") == 0     \
+			 || strcmp (Fs_type, "smb3") == 0   \
+			 || strcmp (Fs_type, "cifs") == 0)) \
+	 || strcmp (Fs_type, "acfs") == 0           \
+	 || strcmp (Fs_type, "afs") == 0            \
+	 || strcmp (Fs_type, "coda") == 0           \
+	 || strcmp (Fs_type, "auristorfs") == 0     \
+	 || strcmp (Fs_type, "fhgfs") == 0          \
+	 || strcmp (Fs_type, "gpfs") == 0           \
+	 || strcmp (Fs_type, "ibrix") == 0          \
+	 || strcmp (Fs_type, "ocfs2") == 0          \
+	 || strcmp (Fs_type, "vxfs") == 0           \
+	 || strcmp ("-hosts", Fs_name) == 0)
+#endif
+
+static int find_mount(const char *path, const struct statvfs *fs,
+	struct mntent *ent)
+{
+	const char *const mounts = "/proc/mounts";
+	const char *rp = real_pathdup(path, 1);
+	struct mntent *ment = NULL;
+	struct statvfs mntfs;
+	FILE *fp;
+	int found = 0;
+	int dlen, plen, flen = 0;
+
+	ent->mnt_fsname = NULL;
+	ent->mnt_dir = NULL;
+	ent->mnt_type = NULL;
+
+	fp = setmntent(mounts, "r");
+	if (!fp) {
+		error_errno(_("setmntent('%s') failed"), mounts);
+		return -1;
+	}
+
+	plen = strlen(rp);
+
+	/* read all the mount information and compare to path */
+	while ((ment = getmntent(fp)) != NULL) {
+		if (statvfs(ment->mnt_dir, &mntfs)) {
+			switch (errno) {
+			case EPERM:
+			case ESRCH:
+			case EACCES:
+				continue;
+			default:
+				error_errno(_("statvfs('%s') failed"), ment->mnt_dir);
+				endmntent(fp);
+				return -1;
+			}
+		}
+
+		/* is mount on the same filesystem and is a prefix of the path */
+		if ((fs->f_fsid == mntfs.f_fsid) &&
+			!strncmp(ment->mnt_dir, rp, strlen(ment->mnt_dir))) {
+			dlen = strlen(ment->mnt_dir);
+			if (dlen > plen)
+				continue;
+			/*
+			 * root is always a potential match; otherwise look for
+			 * directory prefix
+			 */
+			if ((dlen == 1 && ment->mnt_dir[0] == '/') ||
+				(dlen > flen && (!rp[dlen] || rp[dlen] == '/'))) {
+				flen = dlen;
+				/*
+				 * https://man7.org/linux/man-pages/man3/getmntent.3.html
+				 *
+				 * The pointer points to a static area of memory which is
+				 * overwritten by subsequent calls to getmntent().
+				 */
+				found = 1;
+				free(ent->mnt_fsname);
+				free(ent->mnt_dir);
+				free(ent->mnt_type);
+				ent->mnt_fsname = xstrdup(ment->mnt_fsname);
+				ent->mnt_dir = xstrdup(ment->mnt_dir);
+				ent->mnt_type = xstrdup(ment->mnt_type);
+			}
+		}
+	}
+	endmntent(fp);
+
+	if (!found)
+		return -1;
+
+	return 0;
+}
+
+int fsmonitor__get_fs_info(const char *path, struct fs_info *fs_info)
+{
+	struct mntent ment;
+	struct statvfs fs;
+
+	if (statvfs(path, &fs))
+		return error_errno(_("statvfs('%s') failed"), path);
+
+
+	if (find_mount(path, &fs, &ment) < 0) {
+		free(ment.mnt_fsname);
+		free(ment.mnt_dir);
+		free(ment.mnt_type);
+		return -1;
+	}
+
+	trace_printf_key(&trace_fsmonitor,
+			 "statvfs('%s') [flags 0x%08lx] '%s' '%s'",
+			 path, fs.f_flag, ment.mnt_type, ment.mnt_fsname);
+
+	fs_info->is_remote = ME_REMOTE(ment.mnt_fsname, ment.mnt_type);
+	fs_info->typename = ment.mnt_fsname;
+	free(ment.mnt_dir);
+	free(ment.mnt_type);
+
+	trace_printf_key(&trace_fsmonitor,
+				"'%s' is_remote: %d",
+				path, fs_info->is_remote);
+	return 0;
+}
+
+int fsmonitor__is_fs_remote(const char *path)
+{
+	struct fs_info fs;
+
+	if (fsmonitor__get_fs_info(path, &fs))
+		return -1;
+
+	free(fs.typename);
+
+	return fs.is_remote;
+}
+
+/*
+ * No-op for now.
+ */
+int fsmonitor__get_alias(const char *path, struct alias_info *info)
+{
+	return 0;
+}
+
+/*
+ * No-op for now.
+ */
+char *fsmonitor__resolve_alias(const char *path,
+	const struct alias_info *info)
+{
+	return NULL;
+}
Signature · 1 line
gitgitgadget
#65 lore raw

PATCH v3 3/6 fsmonitor: implement filesystem change listener for Linux

In reply to Eric DeCosta via GitGitGadget

From: Eric DeCosta <[email protected]>

Implement a filesystem change listener for Linux based on the inotify API:
https://man7.org/linux/man-pages/man7/inotify.7.html

inotify requires registering a watch on every directory in the worktree and special handling of moves/renames.

---
 compat/fsmonitor/fsm-listen-linux.c | 676 ++++++++++++++++++++++++++++
 1 file changed, 676 insertions(+)
 create mode 100644 compat/fsmonitor/fsm-listen-linux.c
compat/fsmonitor/fsm-listen-linux.c new file +676 -0
new file mode 100644
index 00000000000..e8548e4e009
@@ -0,0 +1,676 @@
+#include "cache.h"
+#include "fsmonitor.h"
+#include "fsm-listen.h"
+#include "fsmonitor--daemon.h"
+#include <dirent.h>
+#include <fcntl.h>
+#include <sys/inotify.h>
+#include <sys/stat.h>
+
+/*
+ * Safe value to bitwise OR with rest of mask for
+ * kernels that do not support IN_MASK_CREATE
+ */
+#ifndef IN_MASK_CREATE
+#define IN_MASK_CREATE 0x00000000
+#endif
+
+enum shutdown_reason {
+	SHUTDOWN_CONTINUE = 0,
+	SHUTDOWN_STOP,
+	SHUTDOWN_ERROR,
+	SHUTDOWN_FORCE
+};
+
+struct watch_entry {
+	struct hashmap_entry ent;
+	int wd;
+	uint32_t cookie;
+	const char *dir;
+};
+
+struct rename_entry {
+	struct hashmap_entry ent;
+	time_t whence;
+	uint32_t cookie;
+	const char *dir;
+};
+
+struct fsm_listen_data {
+	int fd_inotify;
+	enum shutdown_reason shutdown;
+	struct hashmap watches;
+	struct hashmap renames;
+	struct hashmap revwatches;
+};
+
+static int watch_entry_cmp(const void *cmp_data,
+			  const struct hashmap_entry *eptr,
+			  const struct hashmap_entry *entry_or_key,
+			  const void *keydata)
+{
+	const struct watch_entry *e1, *e2;
+
+	e1 = container_of(eptr, const struct watch_entry, ent);
+	e2 = container_of(eptr, const struct watch_entry, ent);
+	return e1->wd != e2->wd;
+}
+
+static int revwatches_entry_cmp(const void *cmp_data,
+			  const struct hashmap_entry *eptr,
+			  const struct hashmap_entry *entry_or_key,
+			  const void *keydata)
+{
+	const struct watch_entry *e1, *e2;
+
+	e1 = container_of(eptr, const struct watch_entry, ent);
+	e2 = container_of(eptr, const struct watch_entry, ent);
+	return strcmp(e1->dir, e2->dir);
+}
+
+static int rename_entry_cmp(const void *cmp_data,
+			  const struct hashmap_entry *eptr,
+			  const struct hashmap_entry *entry_or_key,
+			  const void *keydata)
+{
+	const struct rename_entry *e1, *e2;
+
+	e1 = container_of(eptr, const struct rename_entry, ent);
+	e2 = container_of(eptr, const struct rename_entry, ent);
+	return e1->cookie != e2->cookie;
+}
+
+/*
+ * Register an inotify watch, add watch descriptor to path mapping
+ * and the reverse mapping.
+ */
+static int add_watch(const char *path, struct fsm_listen_data *data)
+{
+	const char *interned = strintern(path);
+	struct watch_entry *w1, *w2;
+
+	/* add the inotify watch, don't allow watches to be modified */
+	int wd = inotify_add_watch(data->fd_inotify, interned,
+				(IN_ALL_EVENTS | IN_ONLYDIR | IN_MASK_CREATE)
+				^ IN_ACCESS ^ IN_CLOSE ^ IN_OPEN);
+	if (wd < 0)
+		return error_errno("inotify_add_watch('%s') failed", interned);
+
+	/* add watch descriptor -> directory mapping */
+	CALLOC_ARRAY(w1, 1);
+	w1->wd = wd;
+	w1->dir = interned;
+	hashmap_entry_init(&w1->ent, memhash(&w1->wd, sizeof(int)));
+	hashmap_add(&data->watches, &w1->ent);
+
+	/* add directory -> watch descriptor mapping */
+	CALLOC_ARRAY(w2, 1);
+	w2->wd = wd;
+	w2->dir = interned;
+	hashmap_entry_init(&w2->ent, memhash(w2->dir, strlen(w2->dir)));
+	hashmap_add(&data->revwatches, &w2->ent);
+
+	return 0;
+}
+
+/*
+ * Remove the inotify watch, the watch descriptor to path mapping
+ * and the reverse mapping.
+ */
+static void remove_watch(struct watch_entry *w,
+	struct fsm_listen_data *data)
+{
+	struct watch_entry k1, k2, *w1, *w2;
+
+	/* remove watch, ignore error if kernel already did it */
+	if (inotify_rm_watch(data->fd_inotify, w->wd) && errno != EINVAL)
+		error_errno("inotify_rm_watch() failed");
+
+	hashmap_entry_init(&k1.ent, memhash(&w->wd, sizeof(int)));
+	w1 = hashmap_remove_entry(&data->watches, &k1, ent, NULL);
+	if (!w1)
+		BUG("Double remove of watch for '%s'", w->dir);
+
+	if (w1->cookie)
+		BUG("Removing watch for '%s' which has a pending rename", w1->dir);
+
+	hashmap_entry_init(&k2.ent, memhash(w->dir, strlen(w->dir)));
+	w2 = hashmap_remove_entry(&data->revwatches, &k2, ent, NULL);
+	if (!w2)
+		BUG("Double remove of reverse watch for '%s'", w->dir);
+
+	/* w1->dir and w2->dir are interned strings, we don't own them */
+	free(w1);
+	free(w2);
+}
+
+/*
+ * Check for stale directory renames.
+ *
+ * https://man7.org/linux/man-pages/man7/inotify.7.html
+ *
+ * Allow for some small timeout to account for the fact that insertion of the
+ * IN_MOVED_FROM+IN_MOVED_TO event pair is not atomic, and the possibility that
+ * there may not be any IN_MOVED_TO event.
+ *
+ * If the IN_MOVED_TO event is not received within the timeout then events have
+ * been missed and the monitor is in an inconsistent state with respect to the
+ * filesystem.
+ */
+static int check_stale_dir_renames(struct hashmap *renames, time_t max_age)
+{
+	struct rename_entry *re;
+	struct hashmap_iter iter;
+
+	hashmap_for_each_entry(renames, &iter, re, ent) {
+		if (re->whence <= max_age)
+			return -1;
+	}
+	return 0;
+}
+
+/*
+ * Track pending renames.
+ *
+ * Tracking is done via a event cookie to watch descriptor mapping.
+ *
+ * A rename is not complete until matching a IN_MOVED_TO event is received
+ * for a corresponding IN_MOVED_FROM event.
+ */
+static void add_dir_rename(uint32_t cookie, const char *path,
+	struct fsm_listen_data *data)
+{
+	struct watch_entry k, *w;
+	struct rename_entry *re;
+
+	/* lookup the watch descriptor for the given path */
+	hashmap_entry_init(&k.ent, memhash(path, strlen(path)));
+	w = hashmap_get_entry(&data->revwatches, &k, ent, NULL);
+	if (!w) /* should never happen */
+		BUG("No watch for '%s'", path);
+	w->cookie = cookie;
+
+	/* add the pending rename to match against later */
+	CALLOC_ARRAY(re, 1);
+	re->dir = w->dir;
+	re->cookie = w->cookie;
+	re->whence = time(NULL);
+	hashmap_entry_init(&re->ent, memhash(&re->cookie, sizeof(uint32_t)));
+	hashmap_add(&data->renames, &re->ent);
+}
+
+/*
+ * Handle directory renames
+ *
+ * Once a IN_MOVED_TO event is received, lookup the rename tracking information
+ * via the event cookie and use this information to update the watch.
+ */
+static void rename_dir(uint32_t cookie, const char *path,
+	struct fsm_listen_data *data)
+{
+	struct rename_entry rek, *re;
+	struct watch_entry k, *w;
+
+	/* lookup a pending rename to match */
+	rek.cookie = cookie;
+	hashmap_entry_init(&rek.ent, memhash(&rek.cookie, sizeof(uint32_t)));
+	re = hashmap_get_entry(&data->renames, &rek, ent, NULL);
+	if (re) {
+		k.dir = re->dir;
+		hashmap_entry_init(&k.ent, memhash(k.dir, strlen(k.dir)));
+		w = hashmap_get_entry(&data->revwatches, &k, ent, NULL);
+		if (w) {
+			w->cookie = 0; /* rename handled */
+			remove_watch(w, data);
+			add_watch(path, data);
+		} else {
+			BUG("No matching watch");
+		}
+	} else {
+		BUG("No matching cookie");
+	}
+}
+
+/*
+ * Recursively add watches to every directory under path
+ */
+static int register_inotify(const char *path,
+	struct fsmonitor_daemon_state *state,
+	struct fsmonitor_batch *batch)
+{
+	DIR *dir;
+	const char *rel;
+	struct strbuf current = STRBUF_INIT;
+	struct dirent *de;
+	struct stat fs;
+	int ret = -1;
+
+	dir = opendir(path);
+	if (!dir)
+		return error_errno("opendir('%s') failed", path);
+
+	while ((de = readdir_skip_dot_and_dotdot(dir)) != NULL) {
+		strbuf_reset(&current);
+		strbuf_addf(&current, "%s/%s", path, de->d_name);
+		if (lstat(current.buf, &fs)) {
+			error_errno("lstat('%s') failed", current.buf);
+			goto failed;
+		}
+
+		/* recurse into directory */
+		if (S_ISDIR(fs.st_mode)) {
+			if (add_watch(current.buf, state->listen_data))
+				goto failed;
+			if (register_inotify(current.buf, state, batch))
+				goto failed;
+		} else if (batch) {
+			rel = current.buf + state->path_worktree_watch.len + 1;
+			trace_printf_key(&trace_fsmonitor, "explicitly adding '%s'", rel);
+			fsmonitor_batch__add_path(batch, rel);
+		}
+	}
+	ret = 0;
+
+failed:
+	strbuf_release(&current);
+	if (closedir(dir) < 0)
+		return error_errno("closedir('%s') failed", path);
+	return ret;
+}
+
+static int em_rename_dir_from(u_int32_t mask)
+{
+	return ((mask & IN_ISDIR) && (mask & IN_MOVED_FROM));
+}
+
+static int em_rename_dir_to(u_int32_t mask)
+{
+	return ((mask & IN_ISDIR) && (mask & IN_MOVED_TO));
+}
+
+static int em_remove_watch(u_int32_t mask)
+{
+	return (mask & IN_DELETE_SELF);
+}
+
+static int em_dir_renamed(u_int32_t mask)
+{
+	return ((mask & IN_ISDIR) && (mask & IN_MOVE));
+}
+
+static int em_dir_created(u_int32_t mask)
+{
+	return ((mask & IN_ISDIR) && (mask & IN_CREATE));
+}
+
+static int em_dir_deleted(uint32_t mask)
+{
+	return ((mask & IN_ISDIR) && (mask & IN_DELETE));
+}
+
+static int em_force_shutdown(u_int32_t mask)
+{
+	return (mask & IN_UNMOUNT) || (mask & IN_Q_OVERFLOW);
+}
+
+static int em_ignore(u_int32_t mask)
+{
+	return (mask & IN_IGNORED) || (mask & IN_MOVE_SELF);
+}
+
+static void log_mask_set(const char *path, u_int32_t mask)
+{
+	struct strbuf msg = STRBUF_INIT;
+
+	if (mask & IN_ACCESS)
+		strbuf_addstr(&msg, "IN_ACCESS|");
+	if (mask & IN_MODIFY)
+		strbuf_addstr(&msg, "IN_MODIFY|");
+	if (mask & IN_ATTRIB)
+		strbuf_addstr(&msg, "IN_ATTRIB|");
+	if (mask & IN_CLOSE_WRITE)
+		strbuf_addstr(&msg, "IN_CLOSE_WRITE|");
+	if (mask & IN_CLOSE_NOWRITE)
+		strbuf_addstr(&msg, "IN_CLOSE_NOWRITE|");
+	if (mask & IN_OPEN)
+		strbuf_addstr(&msg, "IN_OPEN|");
+	if (mask & IN_MOVED_FROM)
+		strbuf_addstr(&msg, "IN_MOVED_FROM|");
+	if (mask & IN_MOVED_TO)
+		strbuf_addstr(&msg, "IN_MOVED_TO|");
+	if (mask & IN_CREATE)
+		strbuf_addstr(&msg, "IN_CREATE|");
+	if (mask & IN_DELETE)
+		strbuf_addstr(&msg, "IN_DELETE|");
+	if (mask & IN_DELETE_SELF)
+		strbuf_addstr(&msg, "IN_DELETE_SELF|");
+	if (mask & IN_MOVE_SELF)
+		strbuf_addstr(&msg, "IN_MOVE_SELF|");
+	if (mask & IN_UNMOUNT)
+		strbuf_addstr(&msg, "IN_UNMOUNT|");
+	if (mask & IN_Q_OVERFLOW)
+		strbuf_addstr(&msg, "IN_Q_OVERFLOW|");
+	if (mask & IN_IGNORED)
+		strbuf_addstr(&msg, "IN_IGNORED|");
+	if (mask & IN_ISDIR)
+		strbuf_addstr(&msg, "IN_ISDIR|");
+
+	trace_printf_key(&trace_fsmonitor, "inotify_event: '%s', mask=%#8.8x %s",
+				path, mask, msg.buf);
+
+	strbuf_release(&msg);
+}
+
+int fsm_listen__ctor(struct fsmonitor_daemon_state *state)
+{
+	int fd;
+	int ret = 0;
+	struct fsm_listen_data *data;
+
+	CALLOC_ARRAY(data, 1);
+	state->listen_data = data;
+	state->listen_error_code = -1;
+	data->shutdown = SHUTDOWN_ERROR;
+
+	fd = inotify_init1(O_NONBLOCK);
+	if (fd < 0)
+		return error_errno("inotify_init1() failed");
+
+	data->fd_inotify = fd;
+
+	hashmap_init(&data->watches, watch_entry_cmp, NULL, 0);
+	hashmap_init(&data->renames, rename_entry_cmp, NULL, 0);
+	hashmap_init(&data->revwatches, revwatches_entry_cmp, NULL, 0);
+
+	if (add_watch(state->path_worktree_watch.buf, data))
+		ret = -1;
+	else if (register_inotify(state->path_worktree_watch.buf, state, NULL))
+		ret = -1;
+	else if (state->nr_paths_watching > 1) {
+		if (add_watch(state->path_gitdir_watch.buf, data))
+			ret = -1;
+		else if (register_inotify(state->path_gitdir_watch.buf, state, NULL))
+			ret = -1;
+	}
+
+	if (!ret) {
+		state->listen_error_code = 0;
+		data->shutdown = SHUTDOWN_CONTINUE;
+	}
+
+	return ret;
+}
+
+void fsm_listen__dtor(struct fsmonitor_daemon_state *state)
+{
+	struct fsm_listen_data *data;
+	struct hashmap_iter iter;
+	struct watch_entry *w;
+	int fd;
+
+	if (!state || !state->listen_data)
+		return;
+
+	data = state->listen_data;
+	fd = data->fd_inotify;
+
+	hashmap_for_each_entry(&data->watches, &iter, w, ent) {
+		w->cookie = 0; /* ignore any pending renames */
+		remove_watch(w, data);
+	}
+	hashmap_clear(&data->watches);
+
+	hashmap_clear(&data->revwatches); /* remove_watch freed the entries */
+
+	hashmap_clear_and_free(&data->renames, struct rename_entry, ent);
+
+	FREE_AND_NULL(state->listen_data);
+
+	if (fd && (close(fd) < 0))
+		error_errno(_("closing inotify file descriptor failed"));
+}
+
+void fsm_listen__stop_async(struct fsmonitor_daemon_state *state)
+{
+	if (!state->listen_data->shutdown)
+		state->listen_data->shutdown = SHUTDOWN_STOP;
+}
+
+/*
+ * Process a single inotify event and queue for publication.
+ */
+static int process_event(const char *path,
+	const struct inotify_event *event,
+	struct fsmonitor_batch *batch,
+	struct string_list *cookie_list,
+	struct fsmonitor_daemon_state *state)
+{
+	const char *rel;
+	const char *last_sep;
+
+	switch (fsmonitor_classify_path_absolute(state, path)) {
+		case IS_INSIDE_DOT_GIT_WITH_COOKIE_PREFIX:
+		case IS_INSIDE_GITDIR_WITH_COOKIE_PREFIX:
+			/* Use just the filename of the cookie file. */
+			last_sep = find_last_dir_sep(path);
+			string_list_append(cookie_list,
+					last_sep ? last_sep + 1 : path);
+			break;
+		case IS_INSIDE_DOT_GIT:
+		case IS_INSIDE_GITDIR:
+			break;
+		case IS_DOT_GIT:
+		case IS_GITDIR:
+			/*
+			* If .git directory is deleted or renamed away,
+			* we have to quit.
+			*/
+			if (em_dir_deleted(event->mask)) {
+				trace_printf_key(&trace_fsmonitor,
+						"event: gitdir removed");
+				state->listen_data->shutdown = SHUTDOWN_FORCE;
+				goto done;
+			}
+
+			if (em_dir_renamed(event->mask)) {
+				trace_printf_key(&trace_fsmonitor,
+						"event: gitdir renamed");
+				state->listen_data->shutdown = SHUTDOWN_FORCE;
+				goto done;
+			}
+			break;
+		case IS_WORKDIR_PATH:
+			/* normal events in the working directory */
+			if (trace_pass_fl(&trace_fsmonitor))
+				log_mask_set(path, event->mask);
+
+			rel = path + state->path_worktree_watch.len + 1;
+			fsmonitor_batch__add_path(batch, rel);
+
+			if (em_dir_deleted(event->mask))
+				break;
+
+			/* received IN_MOVE_FROM, add tracking for expected IN_MOVE_TO */
+			if (em_rename_dir_from(event->mask))
+				add_dir_rename(event->cookie, path, state->listen_data);
+
+			/* received IN_MOVE_TO, update watch to reflect new path */
+			if (em_rename_dir_to(event->mask)) {
+				rename_dir(event->cookie, path, state->listen_data);
+				if (register_inotify(path, state, batch)) {
+					state->listen_data->shutdown = SHUTDOWN_ERROR;
+					goto done;
+				}
+			}
+
+			if (em_dir_created(event->mask)) {
+				if (add_watch(path, state->listen_data)) {
+					state->listen_data->shutdown = SHUTDOWN_ERROR;
+					goto done;
+				}
+				if (register_inotify(path, state, batch)) {
+					state->listen_data->shutdown = SHUTDOWN_ERROR;
+					goto done;
+				}
+			}
+			break;
+		case IS_OUTSIDE_CONE:
+		default:
+			trace_printf_key(&trace_fsmonitor,
+					"ignoring '%s'", path);
+			break;
+	}
+	return 0;
+done:
+	return -1;
+}
+
+/*
+ * Read the inotify event stream and pre-process events before further
+ * processing and eventual publishing.
+ */
+static void handle_events(struct fsmonitor_daemon_state *state)
+{
+	 /* See https://man7.org/linux/man-pages/man7/inotify.7.html */
+	char buf[4096]
+		__attribute__ ((aligned(__alignof__(struct inotify_event))));
+
+	struct hashmap watches = state->listen_data->watches;
+	struct fsmonitor_batch *batch = NULL;
+	struct string_list cookie_list = STRING_LIST_INIT_DUP;
+	struct watch_entry k, *w;
+	struct strbuf path;
+	const struct inotify_event *event;
+	int fd = state->listen_data->fd_inotify;
+	ssize_t len;
+	char *ptr, *p;
+
+	strbuf_init(&path, PATH_MAX);
+
+	for(;;) {
+		len = read(fd, buf, sizeof(buf));
+		if (len == -1 && errno != EAGAIN) {
+			error_errno(_("reading inotify message stream failed"));
+			state->listen_data->shutdown = SHUTDOWN_ERROR;
+			goto done;
+		}
+
+		/* nothing to read */
+		if (len <= 0)
+			goto done;
+
+		/* Loop over all events in the buffer. */
+		for (ptr = buf; ptr < buf + len;
+			 ptr += sizeof(struct inotify_event) + event->len) {
+
+			event = (const struct inotify_event *) ptr;
+
+			if (em_ignore(event->mask))
+				continue;
+
+			/* File system was unmounted or event queue overflowed */
+			if (em_force_shutdown(event->mask)) {
+				if (trace_pass_fl(&trace_fsmonitor))
+					log_mask_set("Forcing shutdown", event->mask);
+				state->listen_data->shutdown = SHUTDOWN_FORCE;
+				goto done;
+			}
+
+			hashmap_entry_init(&k.ent, memhash(&event->wd, sizeof(int)));
+			k.wd = event->wd;
+
+			w = hashmap_get_entry(&watches, &k, ent, NULL);
+			if (!w) /* should never happen */
+				BUG("No watch for '%s'", event->name);
+
+			/* directory watch was removed */
+			if (em_remove_watch(event->mask)) {
+				remove_watch(w, state->listen_data);
+				continue;
+			}
+
+			strbuf_reset(&path);
+			strbuf_add(&path, w->dir, strlen(w->dir));
+			strbuf_addch(&path, '/');
+			strbuf_addstr(&path, event->name);
+
+			p = fsmonitor__resolve_alias(path.buf, &state->alias);
+			if (!p)
+				p = strbuf_detach(&path, NULL);
+
+			if (!batch)
+				batch = fsmonitor_batch__new();
+
+			if (process_event(p, event, batch, &cookie_list, state)) {
+				free(p);
+				goto done;
+			}
+			free(p);
+		}
+		strbuf_reset(&path);
+		fsmonitor_publish(state, batch, &cookie_list);
+		string_list_clear(&cookie_list, 0);
+		batch = NULL;
+	}
+done:
+	strbuf_release(&path);
+	fsmonitor_batch__free_list(batch);
+	string_list_clear(&cookie_list, 0);
+}
+
+/*
+ * Non-blocking read of the inotify events stream. The inotify fd is polled
+ * frequently to help minimize the number of queue overflows.
+ */
+void fsm_listen__loop(struct fsmonitor_daemon_state *state)
+{
+	int poll_num;
+	const int interval = 1000;
+	time_t checked = time(NULL);
+	struct pollfd fds[1];
+	fds[0].fd = state->listen_data->fd_inotify;
+	fds[0].events = POLLIN;
+
+	for(;;) {
+		switch (state->listen_data->shutdown) {
+			case SHUTDOWN_CONTINUE:
+				poll_num = poll(fds, 1, 1);
+				if (poll_num == -1) {
+					if (errno == EINTR)
+						continue;
+					error_errno(_("polling inotify message stream failed"));
+					state->listen_data->shutdown = SHUTDOWN_ERROR;
+					continue;
+				}
+
+				if ((time(NULL) - checked) >= interval) {
+					checked = time(NULL);
+					if (check_stale_dir_renames(&state->listen_data->renames,
+						checked - interval)) {
+						trace_printf_key(&trace_fsmonitor,
+							"Missed IN_MOVED_TO events, forcing shutdown");
+						state->listen_data->shutdown = SHUTDOWN_FORCE;
+						continue;
+					}
+				}
+
+				if (poll_num > 0 && (fds[0].revents & POLLIN))
+					handle_events(state);
+
+				continue;
+			case SHUTDOWN_ERROR:
+				state->listen_error_code = -1;
+				ipc_server_stop_async(state->ipc_server_data);
+				break;
+			case SHUTDOWN_FORCE:
+				state->listen_error_code = 0;
+				ipc_server_stop_async(state->ipc_server_data);
+				break;
+			case SHUTDOWN_STOP:
+			default:
+				state->listen_error_code = 0;
+				break;
+		}
+		return;
+	}
+}
Signature · 1 line
gitgitgadget
#66 lore raw

PATCH v3 4/6 fsmonitor: enable fsmonitor for Linux

In reply to Eric DeCosta via GitGitGadget

From: Eric DeCosta <[email protected]>

Update build to enable fsmonitor for Linux.

---
 config.mak.uname                    |  8 ++++++++
 contrib/buildsystems/CMakeLists.txt | 11 ++++++++++-
 2 files changed, 18 insertions(+), 1 deletion(-)
config.mak.uname +8 -0
index d63629fe807..5890d810463 100644
@@ -68,6 +68,14 @@ ifeq ($(uname_S),Linux)
 	ifneq ($(findstring .el7.,$(uname_R)),)
 		BASIC_CFLAGS += -std=c99
 	endif
+	# The builtin FSMonitor on Linux builds upon Simple-IPC.  Both require
+	# Unix domain sockets and PThreads.
+	ifndef NO_PTHREADS
+	ifndef NO_UNIX_SOCKETS
+	FSMONITOR_DAEMON_BACKEND = linux
+	FSMONITOR_OS_SETTINGS = linux
+	endif
+	endif
 endif
 ifeq ($(uname_S),GNU/kFreeBSD)
 	HAVE_ALLOCA_H = YesPlease
contrib/buildsystems/CMakeLists.txt +10 -1
index 3957e4cf8cd..f058c3d19b4 100644
@@ -304,7 +304,16 @@ else()
 endif()
 
 if(SUPPORTS_SIMPLE_IPC)
-	if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
+	if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
+		add_compile_definitions(HAVE_FSMONITOR_DAEMON_BACKEND)
+		list(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-linux.c)
+		list(APPEND compat_SOURCES compat/fsmonitor/fsm-health-linux.c)
+		list(APPEND compat_SOURCES compat/fsmonitor/fsm-ipc-linux.c)
+		list(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-linux.c)
+
+		add_compile_definitions(HAVE_FSMONITOR_OS_SETTINGS)
+		list(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-linux.c)
+	elseif(CMAKE_SYSTEM_NAME STREQUAL "Windows")
 		add_compile_definitions(HAVE_FSMONITOR_DAEMON_BACKEND)
 		list(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-win32.c)
 		list(APPEND compat_SOURCES compat/fsmonitor/fsm-health-win32.c)
Signature · 1 line
gitgitgadget
#67 lore raw

PATCH v3 5/6 fsmonitor: test updates

In reply to Eric DeCosta via GitGitGadget

From: Eric DeCosta <[email protected]>

t7527-builtin-fsmonitor was leaking fsmonitor--daemon processes in some cases.

Accomodate slight difference in the number of events generated on Linux.

On lower-powered systems, spin a little to give the daemon time to respond to and log filesystem events.

---
 t/t7527-builtin-fsmonitor.sh | 94 ++++++++++++++++++++++++++++++------
 1 file changed, 80 insertions(+), 14 deletions(-)
t/t7527-builtin-fsmonitor.sh +80 -14
index 4abc74db2bb..951374231b7 100755
@@ -13,7 +13,7 @@ fi
 stop_daemon_delete_repo () {
 	r=$1 &&
 	test_might_fail git -C $r fsmonitor--daemon stop &&
-	rm -rf $1
+	rm -rf $r
 }
 
 start_daemon () {
@@ -72,6 +72,34 @@ start_daemon () {
 	)
 }
 
+IMPLICIT_TIMEOUT=5
+
+wait_for_update () {
+	func=$1 &&
+	file=$2 &&
+	sz=$(wc -c < "$file") &&
+	last=0 &&
+	$func &&
+	k=0 &&
+	while test "$k" -lt $IMPLICIT_TIMEOUT
+	do
+		nsz=$(wc -c < "$file")
+		if test "$nsz" -gt "$sz"
+		then
+			if test "$last" -eq "$nsz"
+			then
+				cat "$file" &&
+				return 0
+			fi
+			last=$nsz
+		fi
+		sleep 1
+		k=$(( $k + 1 ))
+	done &&
+	cat "$file" &&
+	return 0
+}
+
 # Is a Trace2 data event present with the given catetory and key?
 # We do not care what the value is.
 #
@@ -137,7 +165,6 @@ test_expect_success 'implicit daemon start' '
 # machines (where it might take a moment to wake and reschedule the
 # daemon process) to avoid false alarms during test runs.)
 #
-IMPLICIT_TIMEOUT=5
 
 verify_implicit_shutdown () {
 	r=$1 &&
@@ -373,6 +400,15 @@ create_files () {
 	echo 3 >dir2/new
 }
 
+rename_directory () {
+	mv dirtorename dirrenamed
+}
+
+rename_directory_file () {
+	mv dirtorename dirrenamed &&
+	echo 1 > dirrenamed/new
+}
+
 rename_files () {
 	mv rename renamed &&
 	mv dir1/rename dir1/renamed &&
@@ -427,10 +463,12 @@ test_expect_success 'edit some files' '
 
 	start_daemon --tf "$PWD/.git/trace" &&
 
-	edit_files &&
+	wait_for_update edit_files "$PWD/.git/trace" &&
 
 	test-tool fsmonitor-client query --token 0 &&
 
+	test_might_fail git fsmonitor--daemon stop &&
+
 	grep "^event: dir1/modified$"  .git/trace &&
 	grep "^event: dir2/modified$"  .git/trace &&
 	grep "^event: modified$"       .git/trace &&
@@ -442,10 +480,12 @@ test_expect_success 'create some files' '
 
 	start_daemon --tf "$PWD/.git/trace" &&
 
-	create_files &&
+	wait_for_update create_files "$PWD/.git/trace" &&
 
 	test-tool fsmonitor-client query --token 0 &&
 
+	test_might_fail git fsmonitor--daemon stop &&
+
 	grep "^event: dir1/new$" .git/trace &&
 	grep "^event: dir2/new$" .git/trace &&
 	grep "^event: new$"      .git/trace
@@ -456,10 +496,12 @@ test_expect_success 'delete some files' '
 
 	start_daemon --tf "$PWD/.git/trace" &&
 
-	delete_files &&
+	wait_for_update delete_files "$PWD/.git/trace" &&
 
 	test-tool fsmonitor-client query --token 0 &&
 
+	test_might_fail git fsmonitor--daemon stop &&
+
 	grep "^event: dir1/delete$" .git/trace &&
 	grep "^event: dir2/delete$" .git/trace &&
 	grep "^event: delete$"      .git/trace
@@ -470,10 +512,12 @@ test_expect_success 'rename some files' '
 
 	start_daemon --tf "$PWD/.git/trace" &&
 
-	rename_files &&
+	wait_for_update rename_files "$PWD/.git/trace" &&
 
 	test-tool fsmonitor-client query --token 0 &&
 
+	test_might_fail git fsmonitor--daemon stop &&
+
 	grep "^event: dir1/rename$"  .git/trace &&
 	grep "^event: dir2/rename$"  .git/trace &&
 	grep "^event: rename$"       .git/trace &&
@@ -487,23 +531,42 @@ test_expect_success 'rename directory' '
 
 	start_daemon --tf "$PWD/.git/trace" &&
 
-	mv dirtorename dirrenamed &&
+	wait_for_update rename_directory "$PWD/.git/trace" &&
 
 	test-tool fsmonitor-client query --token 0 &&
 
+	test_might_fail git fsmonitor--daemon stop &&
+
 	grep "^event: dirtorename/*$" .git/trace &&
 	grep "^event: dirrenamed/*$"  .git/trace
 '
 
+test_expect_success 'rename directory file' '
+	test_when_finished clean_up_repo_and_stop_daemon &&
+
+	start_daemon --tf "$PWD/.git/trace" &&
+
+	wait_for_update rename_directory_file "$PWD/.git/trace" &&
+
+	test-tool fsmonitor-client query --token 0 &&
+
+	test_might_fail git fsmonitor--daemon stop &&
+
+	grep "^event: dirtorename/*$" .git/trace &&
+	grep "^event: dirrenamed/*$"  .git/trace &&
+	grep "^event: dirrenamed/new$"  .git/trace
+'
 test_expect_success 'file changes to directory' '
 	test_when_finished clean_up_repo_and_stop_daemon &&
 
 	start_daemon --tf "$PWD/.git/trace" &&
 
-	file_to_directory &&
+	wait_for_update file_to_directory "$PWD/.git/trace" &&
 
 	test-tool fsmonitor-client query --token 0 &&
 
+	test_might_fail git fsmonitor--daemon stop &&
+
 	grep "^event: delete$"     .git/trace &&
 	grep "^event: delete/new$" .git/trace
 '
@@ -513,10 +576,12 @@ test_expect_success 'directory changes to a file' '
 
 	start_daemon --tf "$PWD/.git/trace" &&
 
-	directory_to_file &&
+	wait_for_update directory_to_file "$PWD/.git/trace" &&
 
 	test-tool fsmonitor-client query --token 0 &&
 
+	test_might_fail git fsmonitor--daemon stop &&
+
 	grep "^event: dir1$" .git/trace
 '
 
@@ -561,7 +626,7 @@ test_expect_success 'flush cached data' '
 	test-tool -C test_flush fsmonitor-client query --token "builtin:test_00000002:0" >actual_2 &&
 	nul_to_q <actual_2 >actual_q2 &&
 
-	grep "^builtin:test_00000002:0Q$" actual_q2 &&
+	grep "^builtin:test_00000002:[0-1]Q$" actual_q2 &&
 
 	>test_flush/file_3 &&
 
@@ -732,7 +797,8 @@ u_values="$u1 $u2"
 for u in $u_values
 do
 	test_expect_success "unicode in repo root path: $u" '
-		test_when_finished "stop_daemon_delete_repo $u" &&
+		test_when_finished \
+		"stop_daemon_delete_repo `echo "$u" | sed 's:x:\\\\\\\\\\\\\\x:g'`" &&
 
 		git init "$u" &&
 		echo 1 >"$u"/file1 &&
@@ -818,8 +884,7 @@ test_expect_success 'submodule setup' '
 '
 
 test_expect_success 'submodule always visited' '
-	test_when_finished "git -C super fsmonitor--daemon stop; \
-			    rm -rf super; \
+	test_when_finished "rm -rf super; \
 			    rm -rf sub" &&
 
 	create_super super &&
@@ -887,7 +952,8 @@ have_t2_error_event () {
 }
 
 test_expect_success "stray submodule super-prefix warning" '
-	test_when_finished "rm -rf super; \
+	test_when_finished "git -C super/dir_1/dir_2/sub fsmonitor--daemon stop; \
+			    rm -rf super; \
 			    rm -rf sub;   \
 			    rm super-sub.trace" &&
 
Signature · 1 line
gitgitgadget
#68 lore raw

PATCH v3 6/6 fsmonitor: update doc for Linux

In reply to Eric DeCosta via GitGitGadget

From: Eric DeCosta <[email protected]>

Update the documentation for Linux.

---
 Documentation/config/fsmonitor--daemon.txt |  4 ++--
 Documentation/git-fsmonitor--daemon.txt    | 24 ++++++++++++++--------
 2 files changed, 18 insertions(+), 10 deletions(-)
Documentation/config/fsmonitor--daemon.txt +2 -2
index c225c6c9e74..2cafb040d96 100644
@@ -4,8 +4,8 @@ fsmonitor.allowRemote::
     behavior.  Only respected when `core.fsmonitor` is set to `true`.
 
 fsmonitor.socketDir::
-    This Mac OS-specific option, if set, specifies the directory in
+    Mac OS and Linux-specific option. If set, specifies the directory in
     which to create the Unix domain socket used for communication
     between the fsmonitor daemon and various Git commands. The directory must
-    reside on a native Mac OS filesystem.  Only respected when `core.fsmonitor`
+    reside on a native filesystem.  Only respected when `core.fsmonitor`
     is set to `true`.
Documentation/git-fsmonitor--daemon.txt +16 -8
index 8238eadb0e1..c2b08229c74 100644
@@ -76,23 +76,31 @@ repositories; this may be overridden by setting `fsmonitor.allowRemote` to
 correctly with all network-mounted repositories and such use is considered
 experimental.
 
-On Mac OS, the inter-process communication (IPC) between various Git
+On Linux and Mac OS, the inter-process communication (IPC) between various Git
 commands and the fsmonitor daemon is done via a Unix domain socket (UDS) -- a
-special type of file -- which is supported by native Mac OS filesystems,
-but not on network-mounted filesystems, NTFS, or FAT32.  Other filesystems
-may or may not have the needed support; the fsmonitor daemon is not guaranteed
-to work with these filesystems and such use is considered experimental.
+special type of file -- which is supported by many native Linux and Mac OS
+filesystems, but not on network-mounted filesystems, NTFS, or FAT32.  Other
+filesystems may or may not have the needed support; the fsmonitor daemon is not
+guaranteed to work with these filesystems and such use is considered
+experimental.
 
 By default, the socket is created in the `.git` directory, however, if the
 `.git` directory is on a network-mounted filesystem, it will be instead be
 created at `$HOME/.git-fsmonitor-*` unless `$HOME` itself is on a
 network-mounted filesystem in which case you must set the configuration
-variable `fsmonitor.socketDir` to the path of a directory on a Mac OS native
+variable `fsmonitor.socketDir` to the path of a directory on a native
 filesystem in which to create the socket file.
 
 If none of the above directories (`.git`, `$HOME`, or `fsmonitor.socketDir`)
-is on a native Mac OS file filesystem the fsmonitor daemon will report an
-error that will cause the daemon and the currently running command to exit.
+is on a native Linux or Mac OS filesystem the fsmonitor daemon will report
+an error that will cause the daemon to exit and the currently running command
+to issue a warning.
+
+On Linux, the fsmonitor daemon registers a watch for each directory in the
+repository.  The default per-user limit for the number of watches on most Linux
+systems is 8192.  This may not be sufficient for large repositories or if
+multiple instances of the fsmonitor daemon are running.
+See https://watchexec.github.io/docs/inotify-limits.html[Linux inotify limits] for more information.
 
 CONFIGURATION
 -------------
Signature · 1 line
gitgitgadget
#69 lore raw
In reply to Eric DeCosta via GitGitGadget

[+cc Jeff Hostetler]

Quoted 7 lines · Eric DeCosta via GitGitGadget

Eric DeCosta (6):
fsmonitor: prepare to share code between Mac OS and Linux
fsmonitor: determine if filesystem is local or remote
fsmonitor: implement filesystem change listener for Linux
fsmonitor: enable fsmonitor for Linux
fsmonitor: test updates
fsmonitor: update doc for Linux

Thanks, I picked these up. Let's add Jeff Hostetler (cc'd) to the chain and see if he has any thoughts.

Thanks,
Taylor

#71 lore raw

PATCH v4 1/6 fsmonitor: prepare to share code between Mac OS and Linux

In reply to Eric DeCosta via GitGitGadget

From: Eric DeCosta <[email protected]>

Linux and Mac OS can share some of the code originally developed for Mac OS.

Mac OS and Linux can share fsm-ipc-unix.c and fsm-settings-unix.c

---
 compat/fsmonitor/fsm-health-linux.c    | 24 ++++++++++
 compat/fsmonitor/fsm-ipc-darwin.c      | 53 +---------------------
 compat/fsmonitor/fsm-ipc-linux.c       |  1 +
 compat/fsmonitor/fsm-ipc-unix.c        | 52 +++++++++++++++++++++
 compat/fsmonitor/fsm-settings-darwin.c | 63 +-------------------------
 compat/fsmonitor/fsm-settings-linux.c  |  1 +
 compat/fsmonitor/fsm-settings-unix.c   | 61 +++++++++++++++++++++++++
 7 files changed, 141 insertions(+), 114 deletions(-)
 create mode 100644 compat/fsmonitor/fsm-health-linux.c
 create mode 100644 compat/fsmonitor/fsm-ipc-linux.c
 create mode 100644 compat/fsmonitor/fsm-ipc-unix.c
 create mode 100644 compat/fsmonitor/fsm-settings-linux.c
 create mode 100644 compat/fsmonitor/fsm-settings-unix.c
compat/fsmonitor/fsm-health-linux.c new file +24 -0
new file mode 100644
index 00000000000..b9f709e8548
@@ -0,0 +1,24 @@
+#include "cache.h"
+#include "config.h"
+#include "fsmonitor.h"
+#include "fsm-health.h"
+#include "fsmonitor--daemon.h"
+
+int fsm_health__ctor(struct fsmonitor_daemon_state *state)
+{
+	return 0;
+}
+
+void fsm_health__dtor(struct fsmonitor_daemon_state *state)
+{
+	return;
+}
+
+void fsm_health__loop(struct fsmonitor_daemon_state *state)
+{
+	return;
+}
+
+void fsm_health__stop_async(struct fsmonitor_daemon_state *state)
+{
+}
compat/fsmonitor/fsm-ipc-darwin.c +1 -52

53 lines not on this page · raw message

compat/fsmonitor/fsm-ipc-linux.c new file +1 -0
new file mode 100644
index 00000000000..4c3c92081ee
@@ -0,0 +1 @@
+#include "fsm-ipc-unix.c"
compat/fsmonitor/fsm-ipc-unix.c new file +52 -0

52 lines not on this page · raw message

compat/fsmonitor/fsm-settings-darwin.c +1 -62

63 lines not on this page · raw message

compat/fsmonitor/fsm-settings-linux.c new file +1 -0
new file mode 100644
index 00000000000..14baf9f0603
@@ -0,0 +1 @@
+#include "fsm-settings-unix.c"
compat/fsmonitor/fsm-settings-unix.c new file +61 -0

61 lines not on this page · raw message

Signature · 1 line
gitgitgadget
#70 lore raw

v4 cover fsmonitor: Implement fsmonitor for Linux

In reply to Eric DeCosta via GitGitGadget

Goal is to deliver fsmonitor for Linux that is on par with fsmonitor for Windows and Mac OS.

This patch set builds upon previous work for done for Windows and Mac OS to implement a fsmonitor back-end for Linux based on the Linux inotify API. inotify differs significantly from the equivalent Windows and Mac OS APIs in that a watch must be registered for every directory of interest (rather than a singular watch at the root of the directory tree) and special care must be taken to handle directory renames correctly.

More information about inotify:
https://man7.org/linux/man-pages/man7/inotify.7.html

v3 differs from v2:

* Avoid potential entanglements with GPLv3
* Classify a reasonable set of filesystems as being remote

v2 differs from v1:

* Prior work for Windows and Mac OS has been merged to master, reducing the patch set from 12 to 6 patches
* Code review feedback
* Identified and resolved race condition revealed by CI test system, see "Limitations and caveats" regarding monitoring of directory trees from the man page, above
* Apologies for being away from this for so long, but my attention was needed elsewhere

v1 differs from v0:

* Code review feedback
* Update how and which code can be shared between Mac OS and Linux
* Increase polling frequency to every 1ms (matches Mac OS)
* Updates to t7527 to improve test stability

Eric DeCosta (6):
fsmonitor: prepare to share code between Mac OS and Linux
fsmonitor: determine if filesystem is local or remote
fsmonitor: implement filesystem change listener for Linux
fsmonitor: enable fsmonitor for Linux
fsmonitor: test updates
fsmonitor: update doc for Linux

 Documentation/config/fsmonitor--daemon.txt |   4 +-
 Documentation/git-fsmonitor--daemon.txt    |  24 +-
 compat/fsmonitor/fsm-health-linux.c        |  24 +
 compat/fsmonitor/fsm-ipc-darwin.c          |  53 +-
 compat/fsmonitor/fsm-ipc-linux.c           |   1 +
 compat/fsmonitor/fsm-ipc-unix.c            |  52 ++
 compat/fsmonitor/fsm-listen-linux.c        | 676 +++++++++++++++++++++
 compat/fsmonitor/fsm-path-utils-linux.c    | 186 ++++++
 compat/fsmonitor/fsm-settings-darwin.c     |  63 +-
 compat/fsmonitor/fsm-settings-linux.c      |   1 +
 compat/fsmonitor/fsm-settings-unix.c       |  61 ++
 config.mak.uname                           |   8 +
 contrib/buildsystems/CMakeLists.txt        |  11 +-
 t/t7527-builtin-fsmonitor.sh               |  94 ++-
 14 files changed, 1119 insertions(+), 139 deletions(-)
 create mode 100644 compat/fsmonitor/fsm-health-linux.c
 create mode 100644 compat/fsmonitor/fsm-ipc-linux.c
 create mode 100644 compat/fsmonitor/fsm-ipc-unix.c
 create mode 100644 compat/fsmonitor/fsm-listen-linux.c
 create mode 100644 compat/fsmonitor/fsm-path-utils-linux.c
 create mode 100644 compat/fsmonitor/fsm-settings-linux.c
 create mode 100644 compat/fsmonitor/fsm-settings-unix.c

base-commit: 319605f8f00e402f3ea758a02c63534ff800a711
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-1352%2Fedecosta-mw%2Ffsmonitor_linux-v4
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-1352/edecosta-mw/fsmonitor_linux-v4
Pull-Request: https://github.com/git/git/pull/1352

Range-diff vs v3:

 1:  99d684c7bdf = 1:  99d684c7bdf fsmonitor: prepare to share code between Mac OS and Linux
 2:  aa405379891 ! 2:  e53fc077540 fsmonitor: determine if filesystem is local or remote
     @@ compat/fsmonitor/fsm-path-utils-linux.c (new)
      +#include <errno.h>
      +#include <mntent.h>
      +#include <sys/mount.h>
     ++#include <sys/vfs.h>
      +#include <sys/statvfs.h>
      +
     -+/*
     -+ * https://github.com/coreutils/gnulib/blob/master/lib/mountlist.c
     -+ */
     -+#ifndef ME_REMOTE
     -+/* A file system is "remote" if its Fs_name contains a ':'
     -+   or if (it is of type (smbfs or cifs) and its Fs_name starts with '//')
     -+   or if it is of any other of the listed types
     -+   or Fs_name is equal to "-hosts" (used by autofs to mount remote fs).
     -+   "VM" file systems like prl_fs or vboxsf are not considered remote here. */
     -+# define ME_REMOTE(Fs_name, Fs_type)            \
     -+	(strchr (Fs_name, ':') != NULL              \
     -+	 || ((Fs_name)[0] == '/'                    \
     -+		 && (Fs_name)[1] == '/'                 \
     -+		 && (strcmp (Fs_type, "smbfs") == 0     \
     -+			 || strcmp (Fs_type, "smb3") == 0   \
     -+			 || strcmp (Fs_type, "cifs") == 0)) \
     -+	 || strcmp (Fs_type, "acfs") == 0           \
     -+	 || strcmp (Fs_type, "afs") == 0            \
     -+	 || strcmp (Fs_type, "coda") == 0           \
     -+	 || strcmp (Fs_type, "auristorfs") == 0     \
     -+	 || strcmp (Fs_type, "fhgfs") == 0          \
     -+	 || strcmp (Fs_type, "gpfs") == 0           \
     -+	 || strcmp (Fs_type, "ibrix") == 0          \
     -+	 || strcmp (Fs_type, "ocfs2") == 0          \
     -+	 || strcmp (Fs_type, "vxfs") == 0           \
     -+	 || strcmp ("-hosts", Fs_name) == 0)
     -+#endif
     ++static int is_remote_fs(const char* path) {
     ++	struct statfs fs;
     ++
     ++	if (statfs(path, &fs)) {
     ++		error_errno(_("statfs('%s') failed"), path);
     ++		return -1;
     ++	}
     ++
     ++	switch (fs.f_type) {
     ++		case 0x61636673:  /* ACFS */
     ++		case 0x5346414F:  /* AFS */
     ++		case 0x00C36400:  /* CEPH */
     ++		case 0xFF534D42:  /* CIFS */
     ++		case 0x73757245:  /* CODA */
     ++		case 0x19830326:  /* FHGFS */
     ++		case 0x1161970:   /* GFS */
     ++		case 0x47504653:  /* GPFS */
     ++		case 0x013111A8:  /* IBRIX */
     ++		case 0x6B414653:  /* KAFS */
     ++		case 0x0BD00BD0:  /* LUSTRE */
     ++		case 0x564C:      /* NCP */
     ++		case 0x6969:      /* NFS */
     ++		case 0x6E667364:  /* NFSD */
     ++		case 0x7461636f:  /* OCFS2 */
     ++		case 0xAAD7AAEA:  /* PANFS */
     ++		case 0x517B:      /* SMB */
     ++		case 0xBEEFDEAD:  /* SNFS */
     ++		case 0xFE534D42:  /* SMB2 */
     ++		case 0xBACBACBC:  /* VMHGFS */
     ++		case 0xA501FCF5:  /* VXFS */
     ++			return 1;
     ++		default:
     ++			break;
     ++	}
     ++
     ++	return 0;
     ++}
      +
      +static int find_mount(const char *path, const struct statvfs *fs,
      +	struct mntent *ent)
     @@ compat/fsmonitor/fsm-path-utils-linux.c (new)
      +			 "statvfs('%s') [flags 0x%08lx] '%s' '%s'",
      +			 path, fs.f_flag, ment.mnt_type, ment.mnt_fsname);
      +
     -+	fs_info->is_remote = ME_REMOTE(ment.mnt_fsname, ment.mnt_type);
     ++	fs_info->is_remote = is_remote_fs(ment.mnt_dir);
      +	fs_info->typename = ment.mnt_fsname;
      +	free(ment.mnt_dir);
      +	free(ment.mnt_type);
      +
     ++	if (fs_info->is_remote < 0) {
     ++		free(ment.mnt_fsname);
     ++		return -1;
     ++	}
     ++
      +	trace_printf_key(&trace_fsmonitor,
      +				"'%s' is_remote: %d",
      +				path, fs_info->is_remote);
     ++
      +	return 0;
      +}
      +
 3:  c2e5a7201aa = 3:  80282efef57 fsmonitor: implement filesystem change listener for Linux
 4:  05f5b2dd4fb = 4:  cb03803e355 fsmonitor: enable fsmonitor for Linux
 5:  754355ca44f = 5:  8d9d469b356 fsmonitor: test updates
 6:  f56175e097a = 6:  5afd03fa6ca fsmonitor: update doc for Linux
Signature · 1 line
gitgitgadget
#72 lore raw

PATCH v4 2/6 fsmonitor: determine if filesystem is local or remote

From: Eric DeCosta <[email protected]>

Compare the given path to the mounted filesystems. Find the mount that is the longest prefix of the path (if any) and determine if that mount is on a local or remote filesystem.

---
 compat/fsmonitor/fsm-path-utils-linux.c | 186 ++++++++++++++++++++++++
 1 file changed, 186 insertions(+)
 create mode 100644 compat/fsmonitor/fsm-path-utils-linux.c
compat/fsmonitor/fsm-path-utils-linux.c new file +186 -0
new file mode 100644
index 00000000000..d3281422ebc
@@ -0,0 +1,186 @@
+#include "fsmonitor.h"
+#include "fsmonitor-path-utils.h"
+#include <errno.h>
+#include <mntent.h>
+#include <sys/mount.h>
+#include <sys/vfs.h>
+#include <sys/statvfs.h>
+
+static int is_remote_fs(const char* path) {
+	struct statfs fs;
+
+	if (statfs(path, &fs)) {
+		error_errno(_("statfs('%s') failed"), path);
+		return -1;
+	}
+
+	switch (fs.f_type) {
+		case 0x61636673:  /* ACFS */
+		case 0x5346414F:  /* AFS */
+		case 0x00C36400:  /* CEPH */
+		case 0xFF534D42:  /* CIFS */
+		case 0x73757245:  /* CODA */
+		case 0x19830326:  /* FHGFS */
+		case 0x1161970:   /* GFS */
+		case 0x47504653:  /* GPFS */
+		case 0x013111A8:  /* IBRIX */
+		case 0x6B414653:  /* KAFS */
+		case 0x0BD00BD0:  /* LUSTRE */
+		case 0x564C:      /* NCP */
+		case 0x6969:      /* NFS */
+		case 0x6E667364:  /* NFSD */
+		case 0x7461636f:  /* OCFS2 */
+		case 0xAAD7AAEA:  /* PANFS */
+		case 0x517B:      /* SMB */
+		case 0xBEEFDEAD:  /* SNFS */
+		case 0xFE534D42:  /* SMB2 */
+		case 0xBACBACBC:  /* VMHGFS */
+		case 0xA501FCF5:  /* VXFS */
+			return 1;
+		default:
+			break;
+	}
+
+	return 0;
+}
+
+static int find_mount(const char *path, const struct statvfs *fs,
+	struct mntent *ent)
+{
+	const char *const mounts = "/proc/mounts";
+	const char *rp = real_pathdup(path, 1);
+	struct mntent *ment = NULL;
+	struct statvfs mntfs;
+	FILE *fp;
+	int found = 0;
+	int dlen, plen, flen = 0;
+
+	ent->mnt_fsname = NULL;
+	ent->mnt_dir = NULL;
+	ent->mnt_type = NULL;
+
+	fp = setmntent(mounts, "r");
+	if (!fp) {
+		error_errno(_("setmntent('%s') failed"), mounts);
+		return -1;
+	}
+
+	plen = strlen(rp);
+
+	/* read all the mount information and compare to path */
+	while ((ment = getmntent(fp)) != NULL) {
+		if (statvfs(ment->mnt_dir, &mntfs)) {
+			switch (errno) {
+			case EPERM:
+			case ESRCH:
+			case EACCES:
+				continue;
+			default:
+				error_errno(_("statvfs('%s') failed"), ment->mnt_dir);
+				endmntent(fp);
+				return -1;
+			}
+		}
+
+		/* is mount on the same filesystem and is a prefix of the path */
+		if ((fs->f_fsid == mntfs.f_fsid) &&
+			!strncmp(ment->mnt_dir, rp, strlen(ment->mnt_dir))) {
+			dlen = strlen(ment->mnt_dir);
+			if (dlen > plen)
+				continue;
+			/*
+			 * root is always a potential match; otherwise look for
+			 * directory prefix
+			 */
+			if ((dlen == 1 && ment->mnt_dir[0] == '/') ||
+				(dlen > flen && (!rp[dlen] || rp[dlen] == '/'))) {
+				flen = dlen;
+				/*
+				 * https://man7.org/linux/man-pages/man3/getmntent.3.html
+				 *
+				 * The pointer points to a static area of memory which is
+				 * overwritten by subsequent calls to getmntent().
+				 */
+				found = 1;
+				free(ent->mnt_fsname);
+				free(ent->mnt_dir);
+				free(ent->mnt_type);
+				ent->mnt_fsname = xstrdup(ment->mnt_fsname);
+				ent->mnt_dir = xstrdup(ment->mnt_dir);
+				ent->mnt_type = xstrdup(ment->mnt_type);
+			}
+		}
+	}
+	endmntent(fp);
+
+	if (!found)
+		return -1;
+
+	return 0;
+}
+
+int fsmonitor__get_fs_info(const char *path, struct fs_info *fs_info)
+{
+	struct mntent ment;
+	struct statvfs fs;
+
+	if (statvfs(path, &fs))
+		return error_errno(_("statvfs('%s') failed"), path);
+
+
+	if (find_mount(path, &fs, &ment) < 0) {
+		free(ment.mnt_fsname);
+		free(ment.mnt_dir);
+		free(ment.mnt_type);
+		return -1;
+	}
+
+	trace_printf_key(&trace_fsmonitor,
+			 "statvfs('%s') [flags 0x%08lx] '%s' '%s'",
+			 path, fs.f_flag, ment.mnt_type, ment.mnt_fsname);
+
+	fs_info->is_remote = is_remote_fs(ment.mnt_dir);
+	fs_info->typename = ment.mnt_fsname;
+	free(ment.mnt_dir);
+	free(ment.mnt_type);
+
+	if (fs_info->is_remote < 0) {
+		free(ment.mnt_fsname);
+		return -1;
+	}
+
+	trace_printf_key(&trace_fsmonitor,
+				"'%s' is_remote: %d",
+				path, fs_info->is_remote);
+
+	return 0;
+}
+
+int fsmonitor__is_fs_remote(const char *path)
+{
+	struct fs_info fs;
+
+	if (fsmonitor__get_fs_info(path, &fs))
+		return -1;
+
+	free(fs.typename);
+
+	return fs.is_remote;
+}
+
+/*
+ * No-op for now.
+ */
+int fsmonitor__get_alias(const char *path, struct alias_info *info)
+{
+	return 0;
+}
+
+/*
+ * No-op for now.
+ */
+char *fsmonitor__resolve_alias(const char *path,
+	const struct alias_info *info)
+{
+	return NULL;
+}
Signature · 1 line
gitgitgadget
#75 lore raw

PATCH v4 3/6 fsmonitor: implement filesystem change listener for Linux

In reply to Eric DeCosta via GitGitGadget

From: Eric DeCosta <[email protected]>

Implement a filesystem change listener for Linux based on the inotify API:
https://man7.org/linux/man-pages/man7/inotify.7.html

inotify requires registering a watch on every directory in the worktree and special handling of moves/renames.

---
 compat/fsmonitor/fsm-listen-linux.c | 676 ++++++++++++++++++++++++++++
 1 file changed, 676 insertions(+)
 create mode 100644 compat/fsmonitor/fsm-listen-linux.c
compat/fsmonitor/fsm-listen-linux.c new file +676 -0
new file mode 100644
index 00000000000..e8548e4e009
@@ -0,0 +1,676 @@
+#include "cache.h"
+#include "fsmonitor.h"
+#include "fsm-listen.h"
+#include "fsmonitor--daemon.h"
+#include <dirent.h>
+#include <fcntl.h>
+#include <sys/inotify.h>
+#include <sys/stat.h>
+
+/*
+ * Safe value to bitwise OR with rest of mask for
+ * kernels that do not support IN_MASK_CREATE
+ */
+#ifndef IN_MASK_CREATE
+#define IN_MASK_CREATE 0x00000000
+#endif
+
+enum shutdown_reason {
+	SHUTDOWN_CONTINUE = 0,
+	SHUTDOWN_STOP,
+	SHUTDOWN_ERROR,
+	SHUTDOWN_FORCE
+};
+
+struct watch_entry {
+	struct hashmap_entry ent;
+	int wd;
+	uint32_t cookie;
+	const char *dir;
+};
+
+struct rename_entry {
+	struct hashmap_entry ent;
+	time_t whence;
+	uint32_t cookie;
+	const char *dir;
+};
+
+struct fsm_listen_data {
+	int fd_inotify;
+	enum shutdown_reason shutdown;
+	struct hashmap watches;
+	struct hashmap renames;
+	struct hashmap revwatches;
+};
+
+static int watch_entry_cmp(const void *cmp_data,
+			  const struct hashmap_entry *eptr,
+			  const struct hashmap_entry *entry_or_key,
+			  const void *keydata)
+{
+	const struct watch_entry *e1, *e2;
+
+	e1 = container_of(eptr, const struct watch_entry, ent);
+	e2 = container_of(eptr, const struct watch_entry, ent);
+	return e1->wd != e2->wd;
+}
+
+static int revwatches_entry_cmp(const void *cmp_data,
+			  const struct hashmap_entry *eptr,
+			  const struct hashmap_entry *entry_or_key,
+			  const void *keydata)
+{
+	const struct watch_entry *e1, *e2;
+
+	e1 = container_of(eptr, const struct watch_entry, ent);
+	e2 = container_of(eptr, const struct watch_entry, ent);
+	return strcmp(e1->dir, e2->dir);
+}
+
+static int rename_entry_cmp(const void *cmp_data,
+			  const struct hashmap_entry *eptr,
+			  const struct hashmap_entry *entry_or_key,
+			  const void *keydata)
+{
+	const struct rename_entry *e1, *e2;
+
+	e1 = container_of(eptr, const struct rename_entry, ent);
+	e2 = container_of(eptr, const struct rename_entry, ent);
+	return e1->cookie != e2->cookie;
+}
+
+/*
+ * Register an inotify watch, add watch descriptor to path mapping
+ * and the reverse mapping.
+ */
+static int add_watch(const char *path, struct fsm_listen_data *data)
+{
+	const char *interned = strintern(path);
+	struct watch_entry *w1, *w2;
+
+	/* add the inotify watch, don't allow watches to be modified */
+	int wd = inotify_add_watch(data->fd_inotify, interned,
+				(IN_ALL_EVENTS | IN_ONLYDIR | IN_MASK_CREATE)
+				^ IN_ACCESS ^ IN_CLOSE ^ IN_OPEN);
+	if (wd < 0)
+		return error_errno("inotify_add_watch('%s') failed", interned);
+
+	/* add watch descriptor -> directory mapping */
+	CALLOC_ARRAY(w1, 1);
+	w1->wd = wd;
+	w1->dir = interned;
+	hashmap_entry_init(&w1->ent, memhash(&w1->wd, sizeof(int)));
+	hashmap_add(&data->watches, &w1->ent);
+
+	/* add directory -> watch descriptor mapping */
+	CALLOC_ARRAY(w2, 1);
+	w2->wd = wd;
+	w2->dir = interned;
+	hashmap_entry_init(&w2->ent, memhash(w2->dir, strlen(w2->dir)));
+	hashmap_add(&data->revwatches, &w2->ent);
+
+	return 0;
+}
+
+/*
+ * Remove the inotify watch, the watch descriptor to path mapping
+ * and the reverse mapping.
+ */
+static void remove_watch(struct watch_entry *w,
+	struct fsm_listen_data *data)
+{
+	struct watch_entry k1, k2, *w1, *w2;
+
+	/* remove watch, ignore error if kernel already did it */
+	if (inotify_rm_watch(data->fd_inotify, w->wd) && errno != EINVAL)
+		error_errno("inotify_rm_watch() failed");
+
+	hashmap_entry_init(&k1.ent, memhash(&w->wd, sizeof(int)));
+	w1 = hashmap_remove_entry(&data->watches, &k1, ent, NULL);
+	if (!w1)
+		BUG("Double remove of watch for '%s'", w->dir);
+
+	if (w1->cookie)
+		BUG("Removing watch for '%s' which has a pending rename", w1->dir);
+
+	hashmap_entry_init(&k2.ent, memhash(w->dir, strlen(w->dir)));
+	w2 = hashmap_remove_entry(&data->revwatches, &k2, ent, NULL);
+	if (!w2)
+		BUG("Double remove of reverse watch for '%s'", w->dir);
+
+	/* w1->dir and w2->dir are interned strings, we don't own them */
+	free(w1);
+	free(w2);
+}
+
+/*
+ * Check for stale directory renames.
+ *
+ * https://man7.org/linux/man-pages/man7/inotify.7.html
+ *
+ * Allow for some small timeout to account for the fact that insertion of the
+ * IN_MOVED_FROM+IN_MOVED_TO event pair is not atomic, and the possibility that
+ * there may not be any IN_MOVED_TO event.
+ *
+ * If the IN_MOVED_TO event is not received within the timeout then events have
+ * been missed and the monitor is in an inconsistent state with respect to the
+ * filesystem.
+ */
+static int check_stale_dir_renames(struct hashmap *renames, time_t max_age)
+{
+	struct rename_entry *re;
+	struct hashmap_iter iter;
+
+	hashmap_for_each_entry(renames, &iter, re, ent) {
+		if (re->whence <= max_age)
+			return -1;
+	}
+	return 0;
+}
+
+/*
+ * Track pending renames.
+ *
+ * Tracking is done via a event cookie to watch descriptor mapping.
+ *
+ * A rename is not complete until matching a IN_MOVED_TO event is received
+ * for a corresponding IN_MOVED_FROM event.
+ */
+static void add_dir_rename(uint32_t cookie, const char *path,
+	struct fsm_listen_data *data)
+{
+	struct watch_entry k, *w;
+	struct rename_entry *re;
+
+	/* lookup the watch descriptor for the given path */
+	hashmap_entry_init(&k.ent, memhash(path, strlen(path)));
+	w = hashmap_get_entry(&data->revwatches, &k, ent, NULL);
+	if (!w) /* should never happen */
+		BUG("No watch for '%s'", path);
+	w->cookie = cookie;
+
+	/* add the pending rename to match against later */
+	CALLOC_ARRAY(re, 1);
+	re->dir = w->dir;
+	re->cookie = w->cookie;
+	re->whence = time(NULL);
+	hashmap_entry_init(&re->ent, memhash(&re->cookie, sizeof(uint32_t)));
+	hashmap_add(&data->renames, &re->ent);
+}
+
+/*
+ * Handle directory renames
+ *
+ * Once a IN_MOVED_TO event is received, lookup the rename tracking information
+ * via the event cookie and use this information to update the watch.
+ */
+static void rename_dir(uint32_t cookie, const char *path,
+	struct fsm_listen_data *data)
+{
+	struct rename_entry rek, *re;
+	struct watch_entry k, *w;
+
+	/* lookup a pending rename to match */
+	rek.cookie = cookie;
+	hashmap_entry_init(&rek.ent, memhash(&rek.cookie, sizeof(uint32_t)));
+	re = hashmap_get_entry(&data->renames, &rek, ent, NULL);
+	if (re) {
+		k.dir = re->dir;
+		hashmap_entry_init(&k.ent, memhash(k.dir, strlen(k.dir)));
+		w = hashmap_get_entry(&data->revwatches, &k, ent, NULL);
+		if (w) {
+			w->cookie = 0; /* rename handled */
+			remove_watch(w, data);
+			add_watch(path, data);
+		} else {
+			BUG("No matching watch");
+		}
+	} else {
+		BUG("No matching cookie");
+	}
+}
+
+/*
+ * Recursively add watches to every directory under path
+ */
+static int register_inotify(const char *path,
+	struct fsmonitor_daemon_state *state,
+	struct fsmonitor_batch *batch)
+{
+	DIR *dir;
+	const char *rel;
+	struct strbuf current = STRBUF_INIT;
+	struct dirent *de;
+	struct stat fs;
+	int ret = -1;
+
+	dir = opendir(path);
+	if (!dir)
+		return error_errno("opendir('%s') failed", path);
+
+	while ((de = readdir_skip_dot_and_dotdot(dir)) != NULL) {
+		strbuf_reset(&current);
+		strbuf_addf(&current, "%s/%s", path, de->d_name);
+		if (lstat(current.buf, &fs)) {
+			error_errno("lstat('%s') failed", current.buf);
+			goto failed;
+		}
+
+		/* recurse into directory */
+		if (S_ISDIR(fs.st_mode)) {
+			if (add_watch(current.buf, state->listen_data))
+				goto failed;
+			if (register_inotify(current.buf, state, batch))
+				goto failed;
+		} else if (batch) {
+			rel = current.buf + state->path_worktree_watch.len + 1;
+			trace_printf_key(&trace_fsmonitor, "explicitly adding '%s'", rel);
+			fsmonitor_batch__add_path(batch, rel);
+		}
+	}
+	ret = 0;
+
+failed:
+	strbuf_release(&current);
+	if (closedir(dir) < 0)
+		return error_errno("closedir('%s') failed", path);
+	return ret;
+}
+
+static int em_rename_dir_from(u_int32_t mask)
+{
+	return ((mask & IN_ISDIR) && (mask & IN_MOVED_FROM));
+}
+
+static int em_rename_dir_to(u_int32_t mask)
+{
+	return ((mask & IN_ISDIR) && (mask & IN_MOVED_TO));
+}
+
+static int em_remove_watch(u_int32_t mask)
+{
+	return (mask & IN_DELETE_SELF);
+}
+
+static int em_dir_renamed(u_int32_t mask)
+{
+	return ((mask & IN_ISDIR) && (mask & IN_MOVE));
+}
+
+static int em_dir_created(u_int32_t mask)
+{
+	return ((mask & IN_ISDIR) && (mask & IN_CREATE));
+}
+
+static int em_dir_deleted(uint32_t mask)
+{
+	return ((mask & IN_ISDIR) && (mask & IN_DELETE));
+}
+
+static int em_force_shutdown(u_int32_t mask)
+{
+	return (mask & IN_UNMOUNT) || (mask & IN_Q_OVERFLOW);
+}
+
+static int em_ignore(u_int32_t mask)
+{
+	return (mask & IN_IGNORED) || (mask & IN_MOVE_SELF);
+}
+
+static void log_mask_set(const char *path, u_int32_t mask)
+{
+	struct strbuf msg = STRBUF_INIT;
+
+	if (mask & IN_ACCESS)
+		strbuf_addstr(&msg, "IN_ACCESS|");
+	if (mask & IN_MODIFY)
+		strbuf_addstr(&msg, "IN_MODIFY|");
+	if (mask & IN_ATTRIB)
+		strbuf_addstr(&msg, "IN_ATTRIB|");
+	if (mask & IN_CLOSE_WRITE)
+		strbuf_addstr(&msg, "IN_CLOSE_WRITE|");
+	if (mask & IN_CLOSE_NOWRITE)
+		strbuf_addstr(&msg, "IN_CLOSE_NOWRITE|");
+	if (mask & IN_OPEN)
+		strbuf_addstr(&msg, "IN_OPEN|");
+	if (mask & IN_MOVED_FROM)
+		strbuf_addstr(&msg, "IN_MOVED_FROM|");
+	if (mask & IN_MOVED_TO)
+		strbuf_addstr(&msg, "IN_MOVED_TO|");
+	if (mask & IN_CREATE)
+		strbuf_addstr(&msg, "IN_CREATE|");
+	if (mask & IN_DELETE)
+		strbuf_addstr(&msg, "IN_DELETE|");
+	if (mask & IN_DELETE_SELF)
+		strbuf_addstr(&msg, "IN_DELETE_SELF|");
+	if (mask & IN_MOVE_SELF)
+		strbuf_addstr(&msg, "IN_MOVE_SELF|");
+	if (mask & IN_UNMOUNT)
+		strbuf_addstr(&msg, "IN_UNMOUNT|");
+	if (mask & IN_Q_OVERFLOW)
+		strbuf_addstr(&msg, "IN_Q_OVERFLOW|");
+	if (mask & IN_IGNORED)
+		strbuf_addstr(&msg, "IN_IGNORED|");
+	if (mask & IN_ISDIR)
+		strbuf_addstr(&msg, "IN_ISDIR|");
+
+	trace_printf_key(&trace_fsmonitor, "inotify_event: '%s', mask=%#8.8x %s",
+				path, mask, msg.buf);
+
+	strbuf_release(&msg);
+}
+
+int fsm_listen__ctor(struct fsmonitor_daemon_state *state)
+{
+	int fd;
+	int ret = 0;
+	struct fsm_listen_data *data;
+
+	CALLOC_ARRAY(data, 1);
+	state->listen_data = data;
+	state->listen_error_code = -1;
+	data->shutdown = SHUTDOWN_ERROR;
+
+	fd = inotify_init1(O_NONBLOCK);
+	if (fd < 0)
+		return error_errno("inotify_init1() failed");
+
+	data->fd_inotify = fd;
+
+	hashmap_init(&data->watches, watch_entry_cmp, NULL, 0);
+	hashmap_init(&data->renames, rename_entry_cmp, NULL, 0);
+	hashmap_init(&data->revwatches, revwatches_entry_cmp, NULL, 0);
+
+	if (add_watch(state->path_worktree_watch.buf, data))
+		ret = -1;
+	else if (register_inotify(state->path_worktree_watch.buf, state, NULL))
+		ret = -1;
+	else if (state->nr_paths_watching > 1) {
+		if (add_watch(state->path_gitdir_watch.buf, data))
+			ret = -1;
+		else if (register_inotify(state->path_gitdir_watch.buf, state, NULL))
+			ret = -1;
+	}
+
+	if (!ret) {
+		state->listen_error_code = 0;
+		data->shutdown = SHUTDOWN_CONTINUE;
+	}
+
+	return ret;
+}
+
+void fsm_listen__dtor(struct fsmonitor_daemon_state *state)
+{
+	struct fsm_listen_data *data;
+	struct hashmap_iter iter;
+	struct watch_entry *w;
+	int fd;
+
+	if (!state || !state->listen_data)
+		return;
+
+	data = state->listen_data;
+	fd = data->fd_inotify;
+
+	hashmap_for_each_entry(&data->watches, &iter, w, ent) {
+		w->cookie = 0; /* ignore any pending renames */
+		remove_watch(w, data);
+	}
+	hashmap_clear(&data->watches);
+
+	hashmap_clear(&data->revwatches); /* remove_watch freed the entries */
+
+	hashmap_clear_and_free(&data->renames, struct rename_entry, ent);
+
+	FREE_AND_NULL(state->listen_data);
+
+	if (fd && (close(fd) < 0))
+		error_errno(_("closing inotify file descriptor failed"));
+}
+
+void fsm_listen__stop_async(struct fsmonitor_daemon_state *state)
+{
+	if (!state->listen_data->shutdown)
+		state->listen_data->shutdown = SHUTDOWN_STOP;
+}
+
+/*
+ * Process a single inotify event and queue for publication.
+ */
+static int process_event(const char *path,
+	const struct inotify_event *event,
+	struct fsmonitor_batch *batch,
+	struct string_list *cookie_list,
+	struct fsmonitor_daemon_state *state)
+{
+	const char *rel;
+	const char *last_sep;
+
+	switch (fsmonitor_classify_path_absolute(state, path)) {
+		case IS_INSIDE_DOT_GIT_WITH_COOKIE_PREFIX:
+		case IS_INSIDE_GITDIR_WITH_COOKIE_PREFIX:
+			/* Use just the filename of the cookie file. */
+			last_sep = find_last_dir_sep(path);
+			string_list_append(cookie_list,
+					last_sep ? last_sep + 1 : path);
+			break;
+		case IS_INSIDE_DOT_GIT:
+		case IS_INSIDE_GITDIR:
+			break;
+		case IS_DOT_GIT:
+		case IS_GITDIR:
+			/*
+			* If .git directory is deleted or renamed away,
+			* we have to quit.
+			*/
+			if (em_dir_deleted(event->mask)) {
+				trace_printf_key(&trace_fsmonitor,
+						"event: gitdir removed");
+				state->listen_data->shutdown = SHUTDOWN_FORCE;
+				goto done;
+			}
+
+			if (em_dir_renamed(event->mask)) {
+				trace_printf_key(&trace_fsmonitor,
+						"event: gitdir renamed");
+				state->listen_data->shutdown = SHUTDOWN_FORCE;
+				goto done;
+			}
+			break;
+		case IS_WORKDIR_PATH:
+			/* normal events in the working directory */
+			if (trace_pass_fl(&trace_fsmonitor))
+				log_mask_set(path, event->mask);
+
+			rel = path + state->path_worktree_watch.len + 1;
+			fsmonitor_batch__add_path(batch, rel);
+
+			if (em_dir_deleted(event->mask))
+				break;
+
+			/* received IN_MOVE_FROM, add tracking for expected IN_MOVE_TO */
+			if (em_rename_dir_from(event->mask))
+				add_dir_rename(event->cookie, path, state->listen_data);
+
+			/* received IN_MOVE_TO, update watch to reflect new path */
+			if (em_rename_dir_to(event->mask)) {
+				rename_dir(event->cookie, path, state->listen_data);
+				if (register_inotify(path, state, batch)) {
+					state->listen_data->shutdown = SHUTDOWN_ERROR;
+					goto done;
+				}
+			}
+
+			if (em_dir_created(event->mask)) {
+				if (add_watch(path, state->listen_data)) {
+					state->listen_data->shutdown = SHUTDOWN_ERROR;
+					goto done;
+				}
+				if (register_inotify(path, state, batch)) {
+					state->listen_data->shutdown = SHUTDOWN_ERROR;
+					goto done;
+				}
+			}
+			break;
+		case IS_OUTSIDE_CONE:
+		default:
+			trace_printf_key(&trace_fsmonitor,
+					"ignoring '%s'", path);
+			break;
+	}
+	return 0;
+done:
+	return -1;
+}
+
+/*
+ * Read the inotify event stream and pre-process events before further
+ * processing and eventual publishing.
+ */
+static void handle_events(struct fsmonitor_daemon_state *state)
+{
+	 /* See https://man7.org/linux/man-pages/man7/inotify.7.html */
+	char buf[4096]
+		__attribute__ ((aligned(__alignof__(struct inotify_event))));
+
+	struct hashmap watches = state->listen_data->watches;
+	struct fsmonitor_batch *batch = NULL;
+	struct string_list cookie_list = STRING_LIST_INIT_DUP;
+	struct watch_entry k, *w;
+	struct strbuf path;
+	const struct inotify_event *event;
+	int fd = state->listen_data->fd_inotify;
+	ssize_t len;
+	char *ptr, *p;
+
+	strbuf_init(&path, PATH_MAX);
+
+	for(;;) {
+		len = read(fd, buf, sizeof(buf));
+		if (len == -1 && errno != EAGAIN) {
+			error_errno(_("reading inotify message stream failed"));
+			state->listen_data->shutdown = SHUTDOWN_ERROR;
+			goto done;
+		}
+
+		/* nothing to read */
+		if (len <= 0)
+			goto done;
+
+		/* Loop over all events in the buffer. */
+		for (ptr = buf; ptr < buf + len;
+			 ptr += sizeof(struct inotify_event) + event->len) {
+
+			event = (const struct inotify_event *) ptr;
+
+			if (em_ignore(event->mask))
+				continue;
+
+			/* File system was unmounted or event queue overflowed */
+			if (em_force_shutdown(event->mask)) {
+				if (trace_pass_fl(&trace_fsmonitor))
+					log_mask_set("Forcing shutdown", event->mask);
+				state->listen_data->shutdown = SHUTDOWN_FORCE;
+				goto done;
+			}
+
+			hashmap_entry_init(&k.ent, memhash(&event->wd, sizeof(int)));
+			k.wd = event->wd;
+
+			w = hashmap_get_entry(&watches, &k, ent, NULL);
+			if (!w) /* should never happen */
+				BUG("No watch for '%s'", event->name);
+
+			/* directory watch was removed */
+			if (em_remove_watch(event->mask)) {
+				remove_watch(w, state->listen_data);
+				continue;
+			}
+
+			strbuf_reset(&path);
+			strbuf_add(&path, w->dir, strlen(w->dir));
+			strbuf_addch(&path, '/');
+			strbuf_addstr(&path, event->name);
+
+			p = fsmonitor__resolve_alias(path.buf, &state->alias);
+			if (!p)
+				p = strbuf_detach(&path, NULL);
+
+			if (!batch)
+				batch = fsmonitor_batch__new();
+
+			if (process_event(p, event, batch, &cookie_list, state)) {
+				free(p);
+				goto done;
+			}
+			free(p);
+		}
+		strbuf_reset(&path);
+		fsmonitor_publish(state, batch, &cookie_list);
+		string_list_clear(&cookie_list, 0);
+		batch = NULL;
+	}
+done:
+	strbuf_release(&path);
+	fsmonitor_batch__free_list(batch);
+	string_list_clear(&cookie_list, 0);
+}
+
+/*
+ * Non-blocking read of the inotify events stream. The inotify fd is polled
+ * frequently to help minimize the number of queue overflows.
+ */
+void fsm_listen__loop(struct fsmonitor_daemon_state *state)
+{
+	int poll_num;
+	const int interval = 1000;
+	time_t checked = time(NULL);
+	struct pollfd fds[1];
+	fds[0].fd = state->listen_data->fd_inotify;
+	fds[0].events = POLLIN;
+
+	for(;;) {
+		switch (state->listen_data->shutdown) {
+			case SHUTDOWN_CONTINUE:
+				poll_num = poll(fds, 1, 1);
+				if (poll_num == -1) {
+					if (errno == EINTR)
+						continue;
+					error_errno(_("polling inotify message stream failed"));
+					state->listen_data->shutdown = SHUTDOWN_ERROR;
+					continue;
+				}
+
+				if ((time(NULL) - checked) >= interval) {
+					checked = time(NULL);
+					if (check_stale_dir_renames(&state->listen_data->renames,
+						checked - interval)) {
+						trace_printf_key(&trace_fsmonitor,
+							"Missed IN_MOVED_TO events, forcing shutdown");
+						state->listen_data->shutdown = SHUTDOWN_FORCE;
+						continue;
+					}
+				}
+
+				if (poll_num > 0 && (fds[0].revents & POLLIN))
+					handle_events(state);
+
+				continue;
+			case SHUTDOWN_ERROR:
+				state->listen_error_code = -1;
+				ipc_server_stop_async(state->ipc_server_data);
+				break;
+			case SHUTDOWN_FORCE:
+				state->listen_error_code = 0;
+				ipc_server_stop_async(state->ipc_server_data);
+				break;
+			case SHUTDOWN_STOP:
+			default:
+				state->listen_error_code = 0;
+				break;
+		}
+		return;
+	}
+}
Signature · 1 line
gitgitgadget
#77 lore raw

PATCH v4 4/6 fsmonitor: enable fsmonitor for Linux

In reply to Eric DeCosta via GitGitGadget

From: Eric DeCosta <[email protected]>

Update build to enable fsmonitor for Linux.

---
 config.mak.uname                    |  8 ++++++++
 contrib/buildsystems/CMakeLists.txt | 11 ++++++++++-
 2 files changed, 18 insertions(+), 1 deletion(-)
config.mak.uname +8 -0
index d63629fe807..5890d810463 100644
@@ -68,6 +68,14 @@ ifeq ($(uname_S),Linux)
 	ifneq ($(findstring .el7.,$(uname_R)),)
 		BASIC_CFLAGS += -std=c99
 	endif
+	# The builtin FSMonitor on Linux builds upon Simple-IPC.  Both require
+	# Unix domain sockets and PThreads.
+	ifndef NO_PTHREADS
+	ifndef NO_UNIX_SOCKETS
+	FSMONITOR_DAEMON_BACKEND = linux
+	FSMONITOR_OS_SETTINGS = linux
+	endif
+	endif
 endif
 ifeq ($(uname_S),GNU/kFreeBSD)
 	HAVE_ALLOCA_H = YesPlease
contrib/buildsystems/CMakeLists.txt +10 -1
index 3957e4cf8cd..f058c3d19b4 100644
@@ -304,7 +304,16 @@ else()
 endif()
 
 if(SUPPORTS_SIMPLE_IPC)
-	if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
+	if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
+		add_compile_definitions(HAVE_FSMONITOR_DAEMON_BACKEND)
+		list(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-linux.c)
+		list(APPEND compat_SOURCES compat/fsmonitor/fsm-health-linux.c)
+		list(APPEND compat_SOURCES compat/fsmonitor/fsm-ipc-linux.c)
+		list(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-linux.c)
+
+		add_compile_definitions(HAVE_FSMONITOR_OS_SETTINGS)
+		list(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-linux.c)
+	elseif(CMAKE_SYSTEM_NAME STREQUAL "Windows")
 		add_compile_definitions(HAVE_FSMONITOR_DAEMON_BACKEND)
 		list(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-win32.c)
 		list(APPEND compat_SOURCES compat/fsmonitor/fsm-health-win32.c)
Signature · 1 line
gitgitgadget
#78 lore raw

PATCH v4 5/6 fsmonitor: test updates

In reply to Eric DeCosta via GitGitGadget

From: Eric DeCosta <[email protected]>

t7527-builtin-fsmonitor was leaking fsmonitor--daemon processes in some cases.

Accomodate slight difference in the number of events generated on Linux.

On lower-powered systems, spin a little to give the daemon time to respond to and log filesystem events.

---
 t/t7527-builtin-fsmonitor.sh | 94 ++++++++++++++++++++++++++++++------
 1 file changed, 80 insertions(+), 14 deletions(-)
t/t7527-builtin-fsmonitor.sh +80 -14
index 4abc74db2bb..951374231b7 100755
@@ -13,7 +13,7 @@ fi
 stop_daemon_delete_repo () {
 	r=$1 &&
 	test_might_fail git -C $r fsmonitor--daemon stop &&
-	rm -rf $1
+	rm -rf $r
 }
 
 start_daemon () {
@@ -72,6 +72,34 @@ start_daemon () {
 	)
 }
 
+IMPLICIT_TIMEOUT=5
+
+wait_for_update () {
+	func=$1 &&
+	file=$2 &&
+	sz=$(wc -c < "$file") &&
+	last=0 &&
+	$func &&
+	k=0 &&
+	while test "$k" -lt $IMPLICIT_TIMEOUT
+	do
+		nsz=$(wc -c < "$file")
+		if test "$nsz" -gt "$sz"
+		then
+			if test "$last" -eq "$nsz"
+			then
+				cat "$file" &&
+				return 0
+			fi
+			last=$nsz
+		fi
+		sleep 1
+		k=$(( $k + 1 ))
+	done &&
+	cat "$file" &&
+	return 0
+}
+
 # Is a Trace2 data event present with the given catetory and key?
 # We do not care what the value is.
 #
@@ -137,7 +165,6 @@ test_expect_success 'implicit daemon start' '
 # machines (where it might take a moment to wake and reschedule the
 # daemon process) to avoid false alarms during test runs.)
 #
-IMPLICIT_TIMEOUT=5
 
 verify_implicit_shutdown () {
 	r=$1 &&
@@ -373,6 +400,15 @@ create_files () {
 	echo 3 >dir2/new
 }
 
+rename_directory () {
+	mv dirtorename dirrenamed
+}
+
+rename_directory_file () {
+	mv dirtorename dirrenamed &&
+	echo 1 > dirrenamed/new
+}
+
 rename_files () {
 	mv rename renamed &&
 	mv dir1/rename dir1/renamed &&
@@ -427,10 +463,12 @@ test_expect_success 'edit some files' '
 
 	start_daemon --tf "$PWD/.git/trace" &&
 
-	edit_files &&
+	wait_for_update edit_files "$PWD/.git/trace" &&
 
 	test-tool fsmonitor-client query --token 0 &&
 
+	test_might_fail git fsmonitor--daemon stop &&
+
 	grep "^event: dir1/modified$"  .git/trace &&
 	grep "^event: dir2/modified$"  .git/trace &&
 	grep "^event: modified$"       .git/trace &&
@@ -442,10 +480,12 @@ test_expect_success 'create some files' '
 
 	start_daemon --tf "$PWD/.git/trace" &&
 
-	create_files &&
+	wait_for_update create_files "$PWD/.git/trace" &&
 
 	test-tool fsmonitor-client query --token 0 &&
 
+	test_might_fail git fsmonitor--daemon stop &&
+
 	grep "^event: dir1/new$" .git/trace &&
 	grep "^event: dir2/new$" .git/trace &&
 	grep "^event: new$"      .git/trace
@@ -456,10 +496,12 @@ test_expect_success 'delete some files' '
 
 	start_daemon --tf "$PWD/.git/trace" &&
 
-	delete_files &&
+	wait_for_update delete_files "$PWD/.git/trace" &&
 
 	test-tool fsmonitor-client query --token 0 &&
 
+	test_might_fail git fsmonitor--daemon stop &&
+
 	grep "^event: dir1/delete$" .git/trace &&
 	grep "^event: dir2/delete$" .git/trace &&
 	grep "^event: delete$"      .git/trace
@@ -470,10 +512,12 @@ test_expect_success 'rename some files' '
 
 	start_daemon --tf "$PWD/.git/trace" &&
 
-	rename_files &&
+	wait_for_update rename_files "$PWD/.git/trace" &&
 
 	test-tool fsmonitor-client query --token 0 &&
 
+	test_might_fail git fsmonitor--daemon stop &&
+
 	grep "^event: dir1/rename$"  .git/trace &&
 	grep "^event: dir2/rename$"  .git/trace &&
 	grep "^event: rename$"       .git/trace &&
@@ -487,23 +531,42 @@ test_expect_success 'rename directory' '
 
 	start_daemon --tf "$PWD/.git/trace" &&
 
-	mv dirtorename dirrenamed &&
+	wait_for_update rename_directory "$PWD/.git/trace" &&
 
 	test-tool fsmonitor-client query --token 0 &&
 
+	test_might_fail git fsmonitor--daemon stop &&
+
 	grep "^event: dirtorename/*$" .git/trace &&
 	grep "^event: dirrenamed/*$"  .git/trace
 '
 
+test_expect_success 'rename directory file' '
+	test_when_finished clean_up_repo_and_stop_daemon &&
+
+	start_daemon --tf "$PWD/.git/trace" &&
+
+	wait_for_update rename_directory_file "$PWD/.git/trace" &&
+
+	test-tool fsmonitor-client query --token 0 &&
+
+	test_might_fail git fsmonitor--daemon stop &&
+
+	grep "^event: dirtorename/*$" .git/trace &&
+	grep "^event: dirrenamed/*$"  .git/trace &&
+	grep "^event: dirrenamed/new$"  .git/trace
+'
 test_expect_success 'file changes to directory' '
 	test_when_finished clean_up_repo_and_stop_daemon &&
 
 	start_daemon --tf "$PWD/.git/trace" &&
 
-	file_to_directory &&
+	wait_for_update file_to_directory "$PWD/.git/trace" &&
 
 	test-tool fsmonitor-client query --token 0 &&
 
+	test_might_fail git fsmonitor--daemon stop &&
+
 	grep "^event: delete$"     .git/trace &&
 	grep "^event: delete/new$" .git/trace
 '
@@ -513,10 +576,12 @@ test_expect_success 'directory changes to a file' '
 
 	start_daemon --tf "$PWD/.git/trace" &&
 
-	directory_to_file &&
+	wait_for_update directory_to_file "$PWD/.git/trace" &&
 
 	test-tool fsmonitor-client query --token 0 &&
 
+	test_might_fail git fsmonitor--daemon stop &&
+
 	grep "^event: dir1$" .git/trace
 '
 
@@ -561,7 +626,7 @@ test_expect_success 'flush cached data' '
 	test-tool -C test_flush fsmonitor-client query --token "builtin:test_00000002:0" >actual_2 &&
 	nul_to_q <actual_2 >actual_q2 &&
 
-	grep "^builtin:test_00000002:0Q$" actual_q2 &&
+	grep "^builtin:test_00000002:[0-1]Q$" actual_q2 &&
 
 	>test_flush/file_3 &&
 
@@ -732,7 +797,8 @@ u_values="$u1 $u2"
 for u in $u_values
 do
 	test_expect_success "unicode in repo root path: $u" '
-		test_when_finished "stop_daemon_delete_repo $u" &&
+		test_when_finished \
+		"stop_daemon_delete_repo `echo "$u" | sed 's:x:\\\\\\\\\\\\\\x:g'`" &&
 
 		git init "$u" &&
 		echo 1 >"$u"/file1 &&
@@ -818,8 +884,7 @@ test_expect_success 'submodule setup' '
 '
 
 test_expect_success 'submodule always visited' '
-	test_when_finished "git -C super fsmonitor--daemon stop; \
-			    rm -rf super; \
+	test_when_finished "rm -rf super; \
 			    rm -rf sub" &&
 
 	create_super super &&
@@ -887,7 +952,8 @@ have_t2_error_event () {
 }
 
 test_expect_success "stray submodule super-prefix warning" '
-	test_when_finished "rm -rf super; \
+	test_when_finished "git -C super/dir_1/dir_2/sub fsmonitor--daemon stop; \
+			    rm -rf super; \
 			    rm -rf sub;   \
 			    rm super-sub.trace" &&
 
Signature · 1 line
gitgitgadget
#79 lore raw

PATCH v4 6/6 fsmonitor: update doc for Linux

In reply to Eric DeCosta via GitGitGadget

From: Eric DeCosta <[email protected]>

Update the documentation for Linux.

---
 Documentation/config/fsmonitor--daemon.txt |  4 ++--
 Documentation/git-fsmonitor--daemon.txt    | 24 ++++++++++++++--------
 2 files changed, 18 insertions(+), 10 deletions(-)
Documentation/config/fsmonitor--daemon.txt +2 -2
index c225c6c9e74..2cafb040d96 100644
@@ -4,8 +4,8 @@ fsmonitor.allowRemote::
     behavior.  Only respected when `core.fsmonitor` is set to `true`.
 
 fsmonitor.socketDir::
-    This Mac OS-specific option, if set, specifies the directory in
+    Mac OS and Linux-specific option. If set, specifies the directory in
     which to create the Unix domain socket used for communication
     between the fsmonitor daemon and various Git commands. The directory must
-    reside on a native Mac OS filesystem.  Only respected when `core.fsmonitor`
+    reside on a native filesystem.  Only respected when `core.fsmonitor`
     is set to `true`.
Documentation/git-fsmonitor--daemon.txt +16 -8
index 8238eadb0e1..c2b08229c74 100644
@@ -76,23 +76,31 @@ repositories; this may be overridden by setting `fsmonitor.allowRemote` to
 correctly with all network-mounted repositories and such use is considered
 experimental.
 
-On Mac OS, the inter-process communication (IPC) between various Git
+On Linux and Mac OS, the inter-process communication (IPC) between various Git
 commands and the fsmonitor daemon is done via a Unix domain socket (UDS) -- a
-special type of file -- which is supported by native Mac OS filesystems,
-but not on network-mounted filesystems, NTFS, or FAT32.  Other filesystems
-may or may not have the needed support; the fsmonitor daemon is not guaranteed
-to work with these filesystems and such use is considered experimental.
+special type of file -- which is supported by many native Linux and Mac OS
+filesystems, but not on network-mounted filesystems, NTFS, or FAT32.  Other
+filesystems may or may not have the needed support; the fsmonitor daemon is not
+guaranteed to work with these filesystems and such use is considered
+experimental.
 
 By default, the socket is created in the `.git` directory, however, if the
 `.git` directory is on a network-mounted filesystem, it will be instead be
 created at `$HOME/.git-fsmonitor-*` unless `$HOME` itself is on a
 network-mounted filesystem in which case you must set the configuration
-variable `fsmonitor.socketDir` to the path of a directory on a Mac OS native
+variable `fsmonitor.socketDir` to the path of a directory on a native
 filesystem in which to create the socket file.
 
 If none of the above directories (`.git`, `$HOME`, or `fsmonitor.socketDir`)
-is on a native Mac OS file filesystem the fsmonitor daemon will report an
-error that will cause the daemon and the currently running command to exit.
+is on a native Linux or Mac OS filesystem the fsmonitor daemon will report
+an error that will cause the daemon to exit and the currently running command
+to issue a warning.
+
+On Linux, the fsmonitor daemon registers a watch for each directory in the
+repository.  The default per-user limit for the number of watches on most Linux
+systems is 8192.  This may not be sufficient for large repositories or if
+multiple instances of the fsmonitor daemon are running.
+See https://watchexec.github.io/docs/inotify-limits.html[Linux inotify limits] for more information.
 
 CONFIGURATION
 -------------
Signature · 1 line
gitgitgadget
#73 lore raw

Re: [PATCH v4 2/6] fsmonitor: determine if filesystem is local or remote

In reply to Eric DeCosta via GitGitGadget
Quoted 1 line · Eric DeCosta via GitGitGadget

+static int is_remote_fs(const char* path) {

Style (asterisk sticks to the variable not to the type).

compat/fsmonitor/fsm-path-utils-linux.c line 42

Quoted 8 lines
compat/fsmonitor/fsm-path-utils-linux.c new file +7 -0
+	switch (fs.f_type) {
+		case 0x61636673:  /* ACFS */
...
+		case 0xA501FCF5:  /* VXFS */
+			return 1;
+		default:
+			break;
+	}

Align case/default to switch by de-denting one level, just like you did the switch() statement in find_mount().

compat/fsmonitor/fsm-path-utils-linux.c line 51

Quoted 5 lines
compat/fsmonitor/fsm-path-utils-linux.c new file +5 -0
+static int find_mount(const char *path, const struct statvfs *fs,
+	struct mntent *ent)
+{
+	const char *const mounts = "/proc/mounts";
+	const char *rp = real_pathdup(path, 1);

Nobody seems to free() this once we are done with this function.

compat/fsmonitor/fsm-path-utils-linux.c line 60

Quoted 9 lines
compat/fsmonitor/fsm-path-utils-linux.c new file +9 -0
+	struct mntent *ment = NULL;
+	struct statvfs mntfs;
+	FILE *fp;
+	int found = 0;
+	int dlen, plen, flen = 0;
+
+	ent->mnt_fsname = NULL;
+	ent->mnt_dir = NULL;
+	ent->mnt_type = NULL;

More on this later.

Quoted 40 lines
compat/fsmonitor/fsm-path-utils-linux.c new file +40 -0
+	fp = setmntent(mounts, "r");
+	if (!fp) {
+		error_errno(_("setmntent('%s') failed"), mounts);
+		return -1;
+	}
+
+	plen = strlen(rp);
+
+	/* read all the mount information and compare to path */
+	while ((ment = getmntent(fp)) != NULL) {
+		if (statvfs(ment->mnt_dir, &mntfs)) {
+			switch (errno) {
+			case EPERM:
+			case ESRCH:
+			case EACCES:
+				continue;
+			default:
+				error_errno(_("statvfs('%s') failed"), ment->mnt_dir);
+				endmntent(fp);
+				return -1;
+			}
+		}
+
+		/* is mount on the same filesystem and is a prefix of the path */
+		if ((fs->f_fsid == mntfs.f_fsid) &&
+			!strncmp(ment->mnt_dir, rp, strlen(ment->mnt_dir))) {
+			dlen = strlen(ment->mnt_dir);
+			if (dlen > plen)
+				continue;
+			/*
+			 * root is always a potential match; otherwise look for
+			 * directory prefix
+			 */
+			if ((dlen == 1 && ment->mnt_dir[0] == '/') ||
+				(dlen > flen && (!rp[dlen] || rp[dlen] == '/'))) {
+				flen = dlen;
+				/*
+				 * https://man7.org/linux/man-pages/man3/getmntent.3.html
+				 *
+				 * The pointer points to a static area of memory which is

compat/fsmonitor/fsm-path-utils-linux.c line 111

Quoted 10 lines
compat/fsmonitor/fsm-path-utils-linux.c new file +10 -0
+				 * overwritten by subsequent calls to getmntent().
+				 */
+				found = 1;
+				free(ent->mnt_fsname);
+				free(ent->mnt_dir);
+				free(ent->mnt_type);
+				ent->mnt_fsname = xstrdup(ment->mnt_fsname);
+				ent->mnt_dir = xstrdup(ment->mnt_dir);
+				ent->mnt_type = xstrdup(ment->mnt_type);
+			}

So more than one mount entries could match the given path. This loop implements "the last one wins", but is that a sensible thing to do? Shouldn't it be more like "the longest one, being the most specific, wins"? If /usr mounts on / and /usr/local mounts on /usr, asking for /usr/local/me would want to discover that it is on the filesystem mounted at /usr/local regardless of the order in which getmntent() returns the entries, no?

Quoted 15 lines
compat/fsmonitor/fsm-path-utils-linux.c new file +13 -0
+		}
+	}
+	endmntent(fp);
+
+	if (!found)
+		return -1;
+
+	return 0;
+}
 
 
+
+int fsmonitor__get_fs_info(const char *path, struct fs_info *fs_info)
+{
+	struct mntent ment;

compat/fsmonitor/fsm-path-utils-linux.c line 134

Quoted 10 lines
compat/fsmonitor/fsm-path-utils-linux.c new file +10 -0
+	struct statvfs fs;
+
+	if (statvfs(path, &fs))
+		return error_errno(_("statvfs('%s') failed"), path);
+
+
+	if (find_mount(path, &fs, &ment) < 0) {
+		free(ment.mnt_fsname);
+		free(ment.mnt_dir);
+		free(ment.mnt_type);

It is a good idea to free, but I _think_ the code is easier to follow if you _clear_ ment before calling find_mount(), not in find_mount().

Quoted 5 lines
compat/fsmonitor/fsm-path-utils-linux.c new file +5 -0
+		return -1;
+	}
+
+	trace_printf_key(&trace_fsmonitor,
+			 "statvfs('%s') [flags 0x%08lx] '%s' '%s'",

compat/fsmonitor/fsm-path-utils-linux.c line 149

Quoted 10 lines
compat/fsmonitor/fsm-path-utils-linux.c new file +10 -0
+			 path, fs.f_flag, ment.mnt_type, ment.mnt_fsname);
+
+	fs_info->is_remote = is_remote_fs(ment.mnt_dir);
+	fs_info->typename = ment.mnt_fsname;
+	free(ment.mnt_dir);
+	free(ment.mnt_type);
+
+	if (fs_info->is_remote < 0) {
+		free(ment.mnt_fsname);
+		return -1;

fs_info->typename just started to point into an already freed memory at this point, which is a very safe thing to do to the caller of this function.

Rather, perhaps delay the setting of typename after this statement, which would be more friendly to the caller than telling them that they are not allowed to touch the member when the function returns negative.

Quoted 37 lines
compat/fsmonitor/fsm-path-utils-linux.c new file +37 -0
+	}
+
+	trace_printf_key(&trace_fsmonitor,
+				"'%s' is_remote: %d",
+				path, fs_info->is_remote);
+
+	return 0;
+}
+
+int fsmonitor__is_fs_remote(const char *path)
+{
+	struct fs_info fs;
+
+	if (fsmonitor__get_fs_info(path, &fs))
+		return -1;
+
+	free(fs.typename);
+
+	return fs.is_remote;
+}
+
+/*
+ * No-op for now.
+ */
+int fsmonitor__get_alias(const char *path, struct alias_info *info)
+{
+	return 0;
+}
+
+/*
+ * No-op for now.
+ */
+char *fsmonitor__resolve_alias(const char *path,
+	const struct alias_info *info)
+{
+	return NULL;
+}
#74 lore raw

Re: [PATCH v4 2/6] fsmonitor: determine if filesystem is local or remote

In reply to Eric DeCosta via GitGitGadget
Quoted 23 lines · Eric DeCosta via GitGitGadget

From: Eric DeCosta <[email protected]>

Compare the given path to the mounted filesystems. Find the mount that is the longest prefix of the path (if any) and determine if that mount is on a local or remote filesystem.

---
 compat/fsmonitor/fsm-path-utils-linux.c | 186 ++++++++++++++++++++++++
 1 file changed, 186 insertions(+)
 create mode 100644 compat/fsmonitor/fsm-path-utils-linux.c
compat/fsmonitor/fsm-path-utils-linux.c new file +5 -0
new file mode 100644
index 00000000000..d3281422ebc
@@ -0,0 +1,186 @@
+#include "fsmonitor.h"
+#include "fsmonitor-path-utils.h"
+#include <errno.h>
+#include <mntent.h>
+#include <sys/mount.h>

compat/fsmonitor/fsm-path-utils-linux.c line 15

Quoted 10 lines
compat/fsmonitor/fsm-path-utils-linux.c new file +10 -0
+#include <sys/vfs.h>
+#include <sys/statvfs.h>
+
+static int is_remote_fs(const char* path) {
+	struct statfs fs;
+
+	if (statfs(path, &fs)) {
+		error_errno(_("statfs('%s') failed"), path);
+		return -1;
+	}

Nit: Drop the braces and do:

	if (statfs(...) == -1)
		return error_errno(...)
Quoted 12 lines
compat/fsmonitor/fsm-path-utils-linux.c new file +12 -0
+	switch (fs.f_type) {
+		case 0x61636673:  /* ACFS */
+		case 0x5346414F:  /* AFS */
+		case 0x00C36400:  /* CEPH */
+		case 0xFF534D42:  /* CIFS */
+		case 0x73757245:  /* CODA */
+		case 0x19830326:  /* FHGFS */
+		case 0x1161970:   /* GFS */
+		case 0x47504653:  /* GPFS */
+		case 0x013111A8:  /* IBRIX */
+		case 0x6B414653:  /* KAFS */
+		case 0x0BD00BD0:  /* LUSTRE */

compat/fsmonitor/fsm-path-utils-linux.c line 38

Quoted 10 lines
compat/fsmonitor/fsm-path-utils-linux.c new file +10 -0
+		case 0x564C:      /* NCP */
+		case 0x6969:      /* NFS */
+		case 0x6E667364:  /* NFSD */
+		case 0x7461636f:  /* OCFS2 */
+		case 0xAAD7AAEA:  /* PANFS */
+		case 0x517B:      /* SMB */
+		case 0xBEEFDEAD:  /* SNFS */
+		case 0xFE534D42:  /* SMB2 */
+		case 0xBACBACBC:  /* VMHGFS */
+		case 0xA501FCF5:  /* VXFS */

So, before we'd compare against the name, but to avoid the GPLv3 copy/pasting we're now comparing against the fs.f_type.

If we are hardcoding them, our usual convention is to lower-case hexdigits, so 0xbacbacbc not 0xBACBACBC.

But at least my statfs() manpage documents the named defines in linux/magic.h for most of these. Why not use those?

compat/fsmonitor/fsm-path-utils-linux.c line 41

Quoted 3 lines
compat/fsmonitor/fsm-path-utils-linux.c new file +3 -0
+			return 1;
+		default:
+			break;

You could just "return 0" here, and...

compat/fsmonitor/fsm-path-utils-linux.c line 44

Quoted 3 lines
compat/fsmonitor/fsm-path-utils-linux.c new file +3 -0
+	}
+
+	return 0;

...drop this "return 0".

compat/fsmonitor/fsm-path-utils-linux.c line 48

Quoted 4 lines
compat/fsmonitor/fsm-path-utils-linux.c new file +4 -0
+}
+
+static int find_mount(const char *path, const struct statvfs *fs,
+	struct mntent *ent)

Misindentation.

Quoted 7 lines
compat/fsmonitor/fsm-path-utils-linux.c new file +7 -0
+{
+	const char *const mounts = "/proc/mounts";
+	const char *rp = real_pathdup(path, 1);
+	struct mntent *ment = NULL;
+	struct statvfs mntfs;
+	FILE *fp;
+	int found = 0;

compat/fsmonitor/fsm-path-utils-linux.c line 65

Quoted 10 lines
compat/fsmonitor/fsm-path-utils-linux.c new file +10 -0
+	int dlen, plen, flen = 0;
+
+	ent->mnt_fsname = NULL;
+	ent->mnt_dir = NULL;
+	ent->mnt_type = NULL;
+
+	fp = setmntent(mounts, "r");
+	if (!fp) {
+		error_errno(_("setmntent('%s') failed"), mounts);
+		return -1;

Ditto "return error_errno()"

compat/fsmonitor/fsm-path-utils-linux.c line 68

Quoted 3 lines
compat/fsmonitor/fsm-path-utils-linux.c new file +3 -0
+	}
+
+	plen = strlen(rp);

Let's make "plen", "dlen" and "flen" a "size_t", not "int"

compat/fsmonitor/fsm-path-utils-linux.c line 71

Quoted 3 lines
compat/fsmonitor/fsm-path-utils-linux.c new file +3 -0
+
+	/* read all the mount information and compare to path */
+	while ((ment = getmntent(fp)) != NULL) {

Drop the "!= NULL"

compat/fsmonitor/fsm-path-utils-linux.c line 80

Quoted 9 lines
compat/fsmonitor/fsm-path-utils-linux.c new file +9 -0
+		if (statvfs(ment->mnt_dir, &mntfs)) {
+			switch (errno) {
+			case EPERM:
+			case ESRCH:
+			case EACCES:
+				continue;
+			default:
+				error_errno(_("statvfs('%s') failed"), ment->mnt_dir);
+				endmntent(fp);

Shouldn't we check the endmntent() error too? Now, from the manpage the interface is funny, and always returns 1.

But since this is linux-specific code it seems safe enough to go with it & glibc assumptions and:

	errno = 0;
        endmntent(fp);
        if (errno)
        	return error_errno(....);

I.e. it'll just call fclose(), which might set errno() on failure.

Maybe it's not worth it...

compat/fsmonitor/fsm-path-utils-linux.c line 128

Quoted 2 lines
compat/fsmonitor/fsm-path-utils-linux.c new file +2 -0
+	if (statvfs(path, &fs))
+		return error_errno(_("statvfs('%s') failed"), path);

Here you do use that "return error_errno(...)" pattern...", yay!

Quoted 7 lines
compat/fsmonitor/fsm-path-utils-linux.c new file +7 -0
+
+
+	if (find_mount(path, &fs, &ment) < 0) {
+		free(ment.mnt_fsname);
+		free(ment.mnt_dir);
+		free(ment.mnt_type);
+		return -1;

compat/fsmonitor/fsm-path-utils-linux.c line 145

Quoted 10 lines
compat/fsmonitor/fsm-path-utils-linux.c new file +10 -0
+	}
+
+	trace_printf_key(&trace_fsmonitor,
+			 "statvfs('%s') [flags 0x%08lx] '%s' '%s'",
+			 path, fs.f_flag, ment.mnt_type, ment.mnt_fsname);
+
+	fs_info->is_remote = is_remote_fs(ment.mnt_dir);
+	fs_info->typename = ment.mnt_fsname;
+	free(ment.mnt_dir);
+	free(ment.mnt_type);

If you're going to \n\n-seperate this and the trace_printf_key() above I think moving the second free() here to that "block" would make sense, sinec here is the last time we use mnt_dir, but the last time we used mnt_type was in the trace_printf_key().

But...

compat/fsmonitor/fsm-path-utils-linux.c line 148

Quoted 3 lines
compat/fsmonitor/fsm-path-utils-linux.c new file +3 -0
+
+	if (fs_info->is_remote < 0) {
+		free(ment.mnt_fsname);

...aren't you NULL init-ing these, why not just for all of these:

	goto error;

Then....

compat/fsmonitor/fsm-path-utils-linux.c line 156

Quoted 8 lines
compat/fsmonitor/fsm-path-utils-linux.c new file +8 -0
+		return -1;
+	}
+
+	trace_printf_key(&trace_fsmonitor,
+				"'%s' is_remote: %d",
+				path, fs_info->is_remote);
+
+	return 0;

Have this be:

	int ret = -1; /* earlier */

	ret = 0;
cleanup:
	free(...);
	free(...);
	return ret;

compat/fsmonitor/fsm-path-utils-linux.c line 166

Quoted 10 lines
compat/fsmonitor/fsm-path-utils-linux.c new file +10 -0
+}
+
+int fsmonitor__is_fs_remote(const char *path)
+{
+	struct fs_info fs;
+
+	if (fsmonitor__get_fs_info(path, &fs))
+		return -1;
+
+	free(fs.typename);

This will segfault if you take the part through fsmonitor__get_fs_info() where we don't have the fs.typename yet, i.e. if statfs() fails.

There's the trivial NULL-init way to work around it, but I think this suggests a leaky abstraction. If we fail to get the fs info, then the function itself should have free'd that, shouldn't it?

compat/fsmonitor/fsm-path-utils-linux.c line 183

Quoted 5 lines
compat/fsmonitor/fsm-path-utils-linux.c new file +5 -0
+/*
+ * No-op for now.
+ */
+char *fsmonitor__resolve_alias(const char *path,
+	const struct alias_info *info)

Ditto misindentatione

#76 lore raw

Re: [PATCH v4 3/6] fsmonitor: implement filesystem change listener for Linux

In reply to Eric DeCosta via GitGitGadget
Quoted 6 lines · Eric DeCosta via GitGitGadget

From: Eric DeCosta <[email protected]>
[...]

compat/fsmonitor/fsm-listen-linux.c new file +4 -0
+}
+
+/*
+ * Remove the inotify watch, the watch descriptor to path mapping

compat/fsmonitor/fsm-listen-linux.c line 127

Quoted 10 lines
compat/fsmonitor/fsm-listen-linux.c new file +10 -0
+ * and the reverse mapping.
+ */
+static void remove_watch(struct watch_entry *w,
+	struct fsm_listen_data *data)
+{
+	struct watch_entry k1, k2, *w1, *w2;
+
+	/* remove watch, ignore error if kernel already did it */
+	if (inotify_rm_watch(data->fd_inotify, w->wd) && errno != EINVAL)
+		error_errno("inotify_rm_watch() failed");

If we error_errno(), shouldn't this function have a return value?

compat/fsmonitor/fsm-listen-linux.c line 140

Quoted 13 lines
compat/fsmonitor/fsm-listen-linux.c new file +13 -0
+
+	hashmap_entry_init(&k1.ent, memhash(&w->wd, sizeof(int)));
+	w1 = hashmap_remove_entry(&data->watches, &k1, ent, NULL);
+	if (!w1)
+		BUG("Double remove of watch for '%s'", w->dir);
+
+	if (w1->cookie)
+		BUG("Removing watch for '%s' which has a pending rename", w1->dir);
+
+	hashmap_entry_init(&k2.ent, memhash(w->dir, strlen(w->dir)));
+	w2 = hashmap_remove_entry(&data->revwatches, &k2, ent, NULL);
+	if (!w2)
+		BUG("Double remove of reverse watch for '%s'", w->dir);

For the BUG() additions: Start with lower-case in messages, see CodingGuidelines. I.e. "double remove of ..." etc. Ditto below..

Quoted 15 lines

[...]

compat/fsmonitor/fsm-listen-linux.c new file +14 -0
+/*
+ * Handle directory renames
+ *
+ * Once a IN_MOVED_TO event is received, lookup the rename tracking information
+ * via the event cookie and use this information to update the watch.
+ */
+static void rename_dir(uint32_t cookie, const char *path,
+	struct fsm_listen_data *data)
+{
+	struct rename_entry rek, *re;
+	struct watch_entry k, *w;
+
+	/* lookup a pending rename to match */
+	rek.cookie = cookie;

compat/fsmonitor/fsm-listen-linux.c line 225

Quoted 10 lines
compat/fsmonitor/fsm-listen-linux.c new file +10 -0
+	hashmap_entry_init(&rek.ent, memhash(&rek.cookie, sizeof(uint32_t)));
+	re = hashmap_get_entry(&data->renames, &rek, ent, NULL);
+	if (re) {
+		k.dir = re->dir;
+		hashmap_entry_init(&k.ent, memhash(k.dir, strlen(k.dir)));
+		w = hashmap_get_entry(&data->revwatches, &k, ent, NULL);
+		if (w) {
+			w->cookie = 0; /* rename handled */
+			remove_watch(w, data);
+			add_watch(path, data);

Elsewhere you check the add_watch() return value, but not here?

Quoted 17 lines
compat/fsmonitor/fsm-listen-linux.c new file +17 -0
+		} else {
+			BUG("No matching watch");
+		}
+	} else {
+		BUG("No matching cookie");
+	}
+}
+
+/*
+ * Recursively add watches to every directory under path
+ */
+static int register_inotify(const char *path,
+	struct fsmonitor_daemon_state *state,
+	struct fsmonitor_batch *batch)
+{
+	DIR *dir;
+	const char *rel;

compat/fsmonitor/fsm-listen-linux.c line 252

Quoted 10 lines
compat/fsmonitor/fsm-listen-linux.c new file +10 -0
+	struct strbuf current = STRBUF_INIT;
+	struct dirent *de;
+	struct stat fs;
+	int ret = -1;
+
+	dir = opendir(path);
+	if (!dir)
+		return error_errno("opendir('%s') failed", path);
+
+	while ((de = readdir_skip_dot_and_dotdot(dir)) != NULL) {

ditto drop the "!= NULL"

Quoted 24 lines
compat/fsmonitor/fsm-listen-linux.c new file +24 -0
+	struct strbuf msg = STRBUF_INIT;
+
+	if (mask & IN_ACCESS)
+		strbuf_addstr(&msg, "IN_ACCESS|");
+	if (mask & IN_MODIFY)
+		strbuf_addstr(&msg, "IN_MODIFY|");
+	if (mask & IN_ATTRIB)
+		strbuf_addstr(&msg, "IN_ATTRIB|");
+	if (mask & IN_CLOSE_WRITE)
+		strbuf_addstr(&msg, "IN_CLOSE_WRITE|");
+	if (mask & IN_CLOSE_NOWRITE)
+		strbuf_addstr(&msg, "IN_CLOSE_NOWRITE|");
+	if (mask & IN_OPEN)
+		strbuf_addstr(&msg, "IN_OPEN|");
+	if (mask & IN_MOVED_FROM)
+		strbuf_addstr(&msg, "IN_MOVED_FROM|");
+	if (mask & IN_MOVED_TO)
+		strbuf_addstr(&msg, "IN_MOVED_TO|");
+	if (mask & IN_CREATE)
+		strbuf_addstr(&msg, "IN_CREATE|");
+	if (mask & IN_DELETE)
+		strbuf_addstr(&msg, "IN_DELETE|");
+	if (mask & IN_DELETE_SELF)
+		strbuf_addstr(&msg, "IN_DELETE_SELF|");

compat/fsmonitor/fsm-listen-linux.c line 356

Quoted 10 lines
compat/fsmonitor/fsm-listen-linux.c new file +10 -0
+	if (mask & IN_MOVE_SELF)
+		strbuf_addstr(&msg, "IN_MOVE_SELF|");
+	if (mask & IN_UNMOUNT)
+		strbuf_addstr(&msg, "IN_UNMOUNT|");
+	if (mask & IN_Q_OVERFLOW)
+		strbuf_addstr(&msg, "IN_Q_OVERFLOW|");
+	if (mask & IN_IGNORED)
+		strbuf_addstr(&msg, "IN_IGNORED|");
+	if (mask & IN_ISDIR)
+		strbuf_addstr(&msg, "IN_ISDIR|");

Shouldn't the very last addition to mask omit the "|"?

I think it would be worth just making this a NULL-delimited "int, const char *" array like:

	{
		{ IN_ACCESS, "IN_ACCESS" },
		...
		NULL,
	};

Then looping over it, or maybe not...

Quoted 10 lines
compat/fsmonitor/fsm-listen-linux.c new file +10 -0
+	trace_printf_key(&trace_fsmonitor, "inotify_event: '%s', mask=%#8.8x %s",
+				path, mask, msg.buf);
+
+	strbuf_release(&msg);
+}
+
+int fsm_listen__ctor(struct fsmonitor_daemon_state *state)
+{
+	int fd;
+	int ret = 0;

compat/fsmonitor/fsm-listen-linux.c line 377

Quoted 10 lines
compat/fsmonitor/fsm-listen-linux.c new file +10 -0
+	struct fsm_listen_data *data;
+
+	CALLOC_ARRAY(data, 1);
+	state->listen_data = data;
+	state->listen_error_code = -1;
+	data->shutdown = SHUTDOWN_ERROR;
+
+	fd = inotify_init1(O_NONBLOCK);
+	if (fd < 0)
+		return error_errno("inotify_init1() failed");

Here you leak the "data" you just allocated on error.

compat/fsmonitor/fsm-listen-linux.c line 385

Quoted 8 lines
compat/fsmonitor/fsm-listen-linux.c new file +8 -0
+
+	data->fd_inotify = fd;
+
+	hashmap_init(&data->watches, watch_entry_cmp, NULL, 0);
+	hashmap_init(&data->renames, rename_entry_cmp, NULL, 0);
+	hashmap_init(&data->revwatches, revwatches_entry_cmp, NULL, 0);
+
+	if (add_watch(state->path_worktree_watch.buf, data))

I.e. we should only avoid free()-ing it if we can successfully hand it over to add_watch(), shouldn't we?n

compat/fsmonitor/fsm-listen-linux.c line 388

Quoted 3 lines
compat/fsmonitor/fsm-listen-linux.c new file +3 -0
+		ret = -1;
+	else if (register_inotify(state->path_worktree_watch.buf, state, NULL))
+		ret = -1;

We add {}'s to all if/else if branches if one needs it, see CodingGuidelines.

compat/fsmonitor/fsm-listen-linux.c line 393

Quoted 5 lines
compat/fsmonitor/fsm-listen-linux.c new file +5 -0
+	else if (state->nr_paths_watching > 1) {
+		if (add_watch(state->path_gitdir_watch.buf, data))
+			ret = -1;
+		else if (register_inotify(state->path_gitdir_watch.buf, state, NULL))
+			ret = -1;

Can't this be:

	else if (state->nr_paths_watching > 1 &&
		 (add_watch(...) || register_inotify(...)))
		ret = -1;

compat/fsmonitor/fsm-listen-linux.c line 452

Quoted 2 lines
compat/fsmonitor/fsm-listen-linux.c new file +2 -0
+	switch (fsmonitor_classify_path_absolute(state, path)) {
+		case IS_INSIDE_DOT_GIT_WITH_COOKIE_PREFIX:

Don't indent the "case" label here more than the "switch", see CodingGuidelines.

compat/fsmonitor/fsm-listen-linux.c line 534

Quoted 3 lines
compat/fsmonitor/fsm-listen-linux.c new file +3 -0
+static void handle_events(struct fsmonitor_daemon_state *state)
+{
+	 /* See https://man7.org/linux/man-pages/man7/inotify.7.html */

Extra " " here after the "\t".

Quoted 4 lines
compat/fsmonitor/fsm-listen-linux.c new file +4 -0
+	char buf[4096]
+		__attribute__ ((aligned(__alignof__(struct inotify_event))));
+
+	struct hashmap watches = state->listen_data->watches;

compat/fsmonitor/fsm-listen-linux.c line 548

Quoted 10 lines
compat/fsmonitor/fsm-listen-linux.c new file +10 -0
+	struct fsmonitor_batch *batch = NULL;
+	struct string_list cookie_list = STRING_LIST_INIT_DUP;
+	struct watch_entry k, *w;
+	struct strbuf path;
+	const struct inotify_event *event;
+	int fd = state->listen_data->fd_inotify;
+	ssize_t len;
+	char *ptr, *p;
+
+	strbuf_init(&path, PATH_MAX);

Just use "struct strbuf path = STRBUF_INIT" instead? I.e...

Quoted 37 lines
compat/fsmonitor/fsm-listen-linux.c new file +37 -0
+
+	for(;;) {
+		len = read(fd, buf, sizeof(buf));
+		if (len == -1 && errno != EAGAIN) {
+			error_errno(_("reading inotify message stream failed"));
+			state->listen_data->shutdown = SHUTDOWN_ERROR;
+			goto done;
+		}
+
+		/* nothing to read */
+		if (len <= 0)
+			goto done;
+
+		/* Loop over all events in the buffer. */
+		for (ptr = buf; ptr < buf + len;
+			 ptr += sizeof(struct inotify_event) + event->len) {
+
+			event = (const struct inotify_event *) ptr;
+
+			if (em_ignore(event->mask))
+				continue;
+
+			/* File system was unmounted or event queue overflowed */
+			if (em_force_shutdown(event->mask)) {
+				if (trace_pass_fl(&trace_fsmonitor))
+					log_mask_set("Forcing shutdown", event->mask);
+				state->listen_data->shutdown = SHUTDOWN_FORCE;
+				goto done;
+			}
+
+			hashmap_entry_init(&k.ent, memhash(&event->wd, sizeof(int)));
+			k.wd = event->wd;
+
+			w = hashmap_get_entry(&watches, &k, ent, NULL);
+			if (!w) /* should never happen */
+				BUG("No watch for '%s'", event->name);
+

compat/fsmonitor/fsm-listen-linux.c line 595

Quoted 10 lines
compat/fsmonitor/fsm-listen-linux.c new file +10 -0
+			/* directory watch was removed */
+			if (em_remove_watch(event->mask)) {
+				remove_watch(w, state->listen_data);
+				continue;
+			}
+
+			strbuf_reset(&path);
+			strbuf_add(&path, w->dir, strlen(w->dir));
+			strbuf_addch(&path, '/');
+			strbuf_addstr(&path, event->name);

... we may not even get to this, so we may pointlessly pre-grow it, if we're considering the micro-optimization.

But if we do need it it'll quickly get up to size in the loop, which is probably smaller than PATH_MAX, and paths can exceed PATH_MAX....

Quoted 4 lines
compat/fsmonitor/fsm-listen-linux.c new file +4 -0
+	for(;;) {
+		switch (state->listen_data->shutdown) {
+			case SHUTDOWN_CONTINUE:
+				poll_num = poll(fds, 1, 1);

compat/fsmonitor/fsm-listen-linux.c line 647

Quoted 10 lines
compat/fsmonitor/fsm-listen-linux.c new file +10 -0
+				if (poll_num == -1) {
+					if (errno == EINTR)
+						continue;
+					error_errno(_("polling inotify message stream failed"));
+					state->listen_data->shutdown = SHUTDOWN_ERROR;
+					continue;
+				}
+
+				if ((time(NULL) - checked) >= interval) {
+					checked = time(NULL);

As this is linux-specific code, shouldn't we use the linux-specific API to get the guaranteed atomically growing time here, arther than time()?

Maybe not, but I wonder if this has funny side-effects with ntpd adjusting the time concurrently...

#80 lore raw

v5 cover fsmonitor: Implement fsmonitor for Linux

In reply to Eric DeCosta via GitGitGadget

Goal is to deliver fsmonitor for Linux that is on par with fsmonitor for Windows and Mac OS.

This patch set builds upon previous work for done for Windows and Mac OS to implement a fsmonitor back-end for Linux based on the Linux inotify API. inotify differs significantly from the equivalent Windows and Mac OS APIs in that a watch must be registered for every directory of interest (rather than a singular watch at the root of the directory tree) and special care must be taken to handle directory renames correctly.

More information about inotify:
https://man7.org/linux/man-pages/man7/inotify.7.html

v4 differs from v3:

* Code review feedback

v3 differs from v2:

* Avoid potential entanglements with GPLv3
* Classify a reasonable set of filesystems as being remote

v2 differs from v1:

* Prior work for Windows and Mac OS has been merged to master, reducing the patch set from 12 to 6 patches
* Code review feedback
* Identified and resolved race condition revealed by CI test system, see "Limitations and caveats" regarding monitoring of directory trees from the man page, above
* Apologies for being away from this for so long, but my attention was needed elsewhere

v1 differs from v0:

* Code review feedback
* Update how and which code can be shared between Mac OS and Linux
* Increase polling frequency to every 1ms (matches Mac OS)
* Updates to t7527 to improve test stability

Eric DeCosta (6):
fsmonitor: prepare to share code between Mac OS and Linux
fsmonitor: determine if filesystem is local or remote
fsmonitor: implement filesystem change listener for Linux
fsmonitor: enable fsmonitor for Linux
fsmonitor: test updates
fsmonitor: update doc for Linux

 Documentation/config/fsmonitor--daemon.txt |   4 +-
 Documentation/git-fsmonitor--daemon.txt    |  24 +-
 compat/fsmonitor/fsm-health-linux.c        |  24 +
 compat/fsmonitor/fsm-ipc-darwin.c          |  53 +-
 compat/fsmonitor/fsm-ipc-linux.c           |   1 +
 compat/fsmonitor/fsm-ipc-unix.c            |  52 ++
 compat/fsmonitor/fsm-listen-linux.c        | 676 +++++++++++++++++++++
 compat/fsmonitor/fsm-path-utils-linux.c    | 193 ++++++
 compat/fsmonitor/fsm-settings-darwin.c     |  63 +-
 compat/fsmonitor/fsm-settings-linux.c      |   1 +
 compat/fsmonitor/fsm-settings-unix.c       |  62 ++
 config.mak.uname                           |   8 +
 contrib/buildsystems/CMakeLists.txt        |  11 +-
 t/t7527-builtin-fsmonitor.sh               |  94 ++-
 14 files changed, 1127 insertions(+), 139 deletions(-)
 create mode 100644 compat/fsmonitor/fsm-health-linux.c
 create mode 100644 compat/fsmonitor/fsm-ipc-linux.c
 create mode 100644 compat/fsmonitor/fsm-ipc-unix.c
 create mode 100644 compat/fsmonitor/fsm-listen-linux.c
 create mode 100644 compat/fsmonitor/fsm-path-utils-linux.c
 create mode 100644 compat/fsmonitor/fsm-settings-linux.c
 create mode 100644 compat/fsmonitor/fsm-settings-unix.c

base-commit: c48035d29b4e524aed3a32f0403676f0d9128863
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-1352%2Fedecosta-mw%2Ffsmonitor_linux-v5
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-1352/edecosta-mw/fsmonitor_linux-v5
Pull-Request: https://github.com/git/git/pull/1352

Range-diff vs v4:

1: 99d684c7bdf ! 1: 7f659603c9a fsmonitor: prepare to share code between Mac OS and Linux

     @@ compat/fsmonitor/fsm-settings-unix.c (new)
      +	strbuf_addstr(&path, ipc_path);
      +
      +	if (fsmonitor__get_fs_info(dirname(path.buf), &fs) == -1) {
     ++		free(fs.typename);
      +		strbuf_release(&path);
      +		return FSMONITOR_REASON_ERROR;
      +	}
 2:  e53fc077540 ! 2:  eb3ff9d1c05 fsmonitor: determine if filesystem is local or remote
     @@ compat/fsmonitor/fsm-path-utils-linux.c (new)
      +#include <sys/vfs.h>
      +#include <sys/statvfs.h>
      +
     -+static int is_remote_fs(const char* path) {
     ++static int is_remote_fs(const char *path) {
      +	struct statfs fs;
      +
      +	if (statfs(path, &fs)) {
     @@ compat/fsmonitor/fsm-path-utils-linux.c (new)
      +	}
      +
      +	switch (fs.f_type) {
     -+		case 0x61636673:  /* ACFS */
     -+		case 0x5346414F:  /* AFS */
     -+		case 0x00C36400:  /* CEPH */
     -+		case 0xFF534D42:  /* CIFS */
     -+		case 0x73757245:  /* CODA */
     -+		case 0x19830326:  /* FHGFS */
     -+		case 0x1161970:   /* GFS */
     -+		case 0x47504653:  /* GPFS */
     -+		case 0x013111A8:  /* IBRIX */
     -+		case 0x6B414653:  /* KAFS */
     -+		case 0x0BD00BD0:  /* LUSTRE */
     -+		case 0x564C:      /* NCP */
     -+		case 0x6969:      /* NFS */
     -+		case 0x6E667364:  /* NFSD */
     -+		case 0x7461636f:  /* OCFS2 */
     -+		case 0xAAD7AAEA:  /* PANFS */
     -+		case 0x517B:      /* SMB */
     -+		case 0xBEEFDEAD:  /* SNFS */
     -+		case 0xFE534D42:  /* SMB2 */
     -+		case 0xBACBACBC:  /* VMHGFS */
     -+		case 0xA501FCF5:  /* VXFS */
     -+			return 1;
     -+		default:
     -+			break;
     ++	case 0x61636673:  /* ACFS */
     ++	case 0x5346414F:  /* AFS */
     ++	case 0x00C36400:  /* CEPH */
     ++	case 0xFF534D42:  /* CIFS */
     ++	case 0x73757245:  /* CODA */
     ++	case 0x19830326:  /* FHGFS */
     ++	case 0x1161970:   /* GFS */
     ++	case 0x47504653:  /* GPFS */
     ++	case 0x013111A8:  /* IBRIX */
     ++	case 0x6B414653:  /* KAFS */
     ++	case 0x0BD00BD0:  /* LUSTRE */
     ++	case 0x564C:      /* NCP */
     ++	case 0x6969:      /* NFS */
     ++	case 0x6E667364:  /* NFSD */
     ++	case 0x7461636f:  /* OCFS2 */
     ++	case 0xAAD7AAEA:  /* PANFS */
     ++	case 0x517B:      /* SMB */
     ++	case 0xBEEFDEAD:  /* SNFS */
     ++	case 0xFE534D42:  /* SMB2 */
     ++	case 0xBACBACBC:  /* VMHGFS */
     ++	case 0xA501FCF5:  /* VXFS */
     ++		return 1;
     ++	default:
     ++		break;
      +	}
      +
      +	return 0;
      +}
      +
      +static int find_mount(const char *path, const struct statvfs *fs,
     -+	struct mntent *ent)
     ++	struct mntent *entry)
      +{
      +	const char *const mounts = "/proc/mounts";
     -+	const char *rp = real_pathdup(path, 1);
     ++	char *rp = real_pathdup(path, 1);
      +	struct mntent *ment = NULL;
      +	struct statvfs mntfs;
      +	FILE *fp;
      +	int found = 0;
      +	int dlen, plen, flen = 0;
      +
     -+	ent->mnt_fsname = NULL;
     -+	ent->mnt_dir = NULL;
     -+	ent->mnt_type = NULL;
     ++	entry->mnt_fsname = NULL;
     ++	entry->mnt_dir = NULL;
     ++	entry->mnt_type = NULL;
      +
      +	fp = setmntent(mounts, "r");
      +	if (!fp) {
     ++		free(rp);
      +		error_errno(_("setmntent('%s') failed"), mounts);
      +		return -1;
      +	}
     @@ compat/fsmonitor/fsm-path-utils-linux.c (new)
      +			default:
      +				error_errno(_("statvfs('%s') failed"), ment->mnt_dir);
      +				endmntent(fp);
     ++				free(rp);
      +				return -1;
      +			}
      +		}
     @@ compat/fsmonitor/fsm-path-utils-linux.c (new)
      +			if (dlen > plen)
      +				continue;
      +			/*
     -+			 * root is always a potential match; otherwise look for
     -+			 * directory prefix
     ++			 * look for the longest prefix (including root)
      +			 */
     -+			if ((dlen == 1 && ment->mnt_dir[0] == '/') ||
     -+				(dlen > flen && (!rp[dlen] || rp[dlen] == '/'))) {
     ++			if (dlen > flen &&
     ++				((dlen == 1 && ment->mnt_dir[0] == '/') ||
     ++				 (!rp[dlen] || rp[dlen] == '/'))) {
      +				flen = dlen;
     ++				found = 1;
     ++
      +				/*
      +				 * https://man7.org/linux/man-pages/man3/getmntent.3.html
      +				 *
      +				 * The pointer points to a static area of memory which is
      +				 * overwritten by subsequent calls to getmntent().
      +				 */
     -+				found = 1;
     -+				free(ent->mnt_fsname);
     -+				free(ent->mnt_dir);
     -+				free(ent->mnt_type);
     -+				ent->mnt_fsname = xstrdup(ment->mnt_fsname);
     -+				ent->mnt_dir = xstrdup(ment->mnt_dir);
     -+				ent->mnt_type = xstrdup(ment->mnt_type);
     ++				free(entry->mnt_fsname);
     ++				free(entry->mnt_dir);
     ++				free(entry->mnt_type);
     ++				entry->mnt_fsname = xstrdup(ment->mnt_fsname);
     ++				entry->mnt_dir = xstrdup(ment->mnt_dir);
     ++				entry->mnt_type = xstrdup(ment->mnt_type);
      +			}
      +		}
      +	}
      +	endmntent(fp);
     ++	free(rp);
      +
      +	if (!found)
      +		return -1;
     @@ compat/fsmonitor/fsm-path-utils-linux.c (new)
      +
      +int fsmonitor__get_fs_info(const char *path, struct fs_info *fs_info)
      +{
     -+	struct mntent ment;
     ++	struct mntent entry;
      +	struct statvfs fs;
      +
     ++	fs_info->is_remote = -1;
     ++	fs_info->typename = NULL;
     ++
      +	if (statvfs(path, &fs))
      +		return error_errno(_("statvfs('%s') failed"), path);
      +
     -+
     -+	if (find_mount(path, &fs, &ment) < 0) {
     -+		free(ment.mnt_fsname);
     -+		free(ment.mnt_dir);
     -+		free(ment.mnt_type);
     ++	if (find_mount(path, &fs, &entry) < 0) {
     ++		free(entry.mnt_fsname);
     ++		free(entry.mnt_dir);
     ++		free(entry.mnt_type);
      +		return -1;
      +	}
      +
      +	trace_printf_key(&trace_fsmonitor,
      +			 "statvfs('%s') [flags 0x%08lx] '%s' '%s'",
     -+			 path, fs.f_flag, ment.mnt_type, ment.mnt_fsname);
     ++			 path, fs.f_flag, entry.mnt_type, entry.mnt_fsname);
      +
     -+	fs_info->is_remote = is_remote_fs(ment.mnt_dir);
     -+	fs_info->typename = ment.mnt_fsname;
     -+	free(ment.mnt_dir);
     -+	free(ment.mnt_type);
     ++	fs_info->is_remote = is_remote_fs(entry.mnt_dir);
     ++	fs_info->typename = xstrdup(entry.mnt_fsname);
     ++	free(entry.mnt_fsname);
     ++	free(entry.mnt_dir);
     ++	free(entry.mnt_type);
      +
     -+	if (fs_info->is_remote < 0) {
     -+		free(ment.mnt_fsname);
     ++	if (fs_info->is_remote < 0)
      +		return -1;
     -+	}
      +
      +	trace_printf_key(&trace_fsmonitor,
      +				"'%s' is_remote: %d",
     @@ compat/fsmonitor/fsm-path-utils-linux.c (new)
      +{
      +	struct fs_info fs;
      +
     -+	if (fsmonitor__get_fs_info(path, &fs))
     ++	if (fsmonitor__get_fs_info(path, &fs)) {
     ++		free(fs.typename);
      +		return -1;
     ++	}
      +
      +	free(fs.typename);
      +
 3:  80282efef57 = 3:  e093a2703b1 fsmonitor: implement filesystem change listener for Linux
 4:  cb03803e355 = 4:  c03070fb0a2 fsmonitor: enable fsmonitor for Linux
 5:  8d9d469b356 = 5:  6a7b554642c fsmonitor: test updates
 6:  5afd03fa6ca = 6:  827410c22ee fsmonitor: update doc for Linux
Signature · 1 line
gitgitgadget
#81 lore raw

PATCH v5 1/6 fsmonitor: prepare to share code between Mac OS and Linux

From: Eric DeCosta <[email protected]>

Linux and Mac OS can share some of the code originally developed for Mac OS.

Mac OS and Linux can share fsm-ipc-unix.c and fsm-settings-unix.c

---
 compat/fsmonitor/fsm-health-linux.c    | 24 ++++++++++
 compat/fsmonitor/fsm-ipc-darwin.c      | 53 +---------------------
 compat/fsmonitor/fsm-ipc-linux.c       |  1 +
 compat/fsmonitor/fsm-ipc-unix.c        | 52 +++++++++++++++++++++
 compat/fsmonitor/fsm-settings-darwin.c | 63 +-------------------------
 compat/fsmonitor/fsm-settings-linux.c  |  1 +
 compat/fsmonitor/fsm-settings-unix.c   | 62 +++++++++++++++++++++++++
 7 files changed, 142 insertions(+), 114 deletions(-)
 create mode 100644 compat/fsmonitor/fsm-health-linux.c
 create mode 100644 compat/fsmonitor/fsm-ipc-linux.c
 create mode 100644 compat/fsmonitor/fsm-ipc-unix.c
 create mode 100644 compat/fsmonitor/fsm-settings-linux.c
 create mode 100644 compat/fsmonitor/fsm-settings-unix.c
compat/fsmonitor/fsm-health-linux.c new file +24 -0
new file mode 100644
index 00000000000..b9f709e8548
@@ -0,0 +1,24 @@
+#include "cache.h"
+#include "config.h"
+#include "fsmonitor.h"
+#include "fsm-health.h"
+#include "fsmonitor--daemon.h"
+
+int fsm_health__ctor(struct fsmonitor_daemon_state *state)
+{
+	return 0;
+}
+
+void fsm_health__dtor(struct fsmonitor_daemon_state *state)
+{
+	return;
+}
+
+void fsm_health__loop(struct fsmonitor_daemon_state *state)
+{
+	return;
+}
+
+void fsm_health__stop_async(struct fsmonitor_daemon_state *state)
+{
+}
compat/fsmonitor/fsm-ipc-darwin.c +1 -52

53 lines not on this page · raw message

compat/fsmonitor/fsm-ipc-linux.c new file +1 -0
new file mode 100644
index 00000000000..4c3c92081ee
@@ -0,0 +1 @@
+#include "fsm-ipc-unix.c"
compat/fsmonitor/fsm-ipc-unix.c new file +52 -0

52 lines not on this page · raw message

compat/fsmonitor/fsm-settings-darwin.c +1 -62

63 lines not on this page · raw message

compat/fsmonitor/fsm-settings-linux.c new file +1 -0
new file mode 100644
index 00000000000..14baf9f0603
@@ -0,0 +1 @@
+#include "fsm-settings-unix.c"
compat/fsmonitor/fsm-settings-unix.c new file +62 -0

62 lines not on this page · raw message

Signature · 1 line
gitgitgadget
#82 lore raw

PATCH v5 2/6 fsmonitor: determine if filesystem is local or remote

In reply to Eric DeCosta via GitGitGadget

From: Eric DeCosta <[email protected]>

Compare the given path to the mounted filesystems. Find the mount that is the longest prefix of the path (if any) and determine if that mount is on a local or remote filesystem.

---
 compat/fsmonitor/fsm-path-utils-linux.c | 193 ++++++++++++++++++++++++
 1 file changed, 193 insertions(+)
 create mode 100644 compat/fsmonitor/fsm-path-utils-linux.c
compat/fsmonitor/fsm-path-utils-linux.c new file +193 -0
new file mode 100644
index 00000000000..629731f75f6
@@ -0,0 +1,193 @@
+#include "fsmonitor.h"
+#include "fsmonitor-path-utils.h"
+#include <errno.h>
+#include <mntent.h>
+#include <sys/mount.h>
+#include <sys/vfs.h>
+#include <sys/statvfs.h>
+
+static int is_remote_fs(const char *path) {
+	struct statfs fs;
+
+	if (statfs(path, &fs)) {
+		error_errno(_("statfs('%s') failed"), path);
+		return -1;
+	}
+
+	switch (fs.f_type) {
+	case 0x61636673:  /* ACFS */
+	case 0x5346414F:  /* AFS */
+	case 0x00C36400:  /* CEPH */
+	case 0xFF534D42:  /* CIFS */
+	case 0x73757245:  /* CODA */
+	case 0x19830326:  /* FHGFS */
+	case 0x1161970:   /* GFS */
+	case 0x47504653:  /* GPFS */
+	case 0x013111A8:  /* IBRIX */
+	case 0x6B414653:  /* KAFS */
+	case 0x0BD00BD0:  /* LUSTRE */
+	case 0x564C:      /* NCP */
+	case 0x6969:      /* NFS */
+	case 0x6E667364:  /* NFSD */
+	case 0x7461636f:  /* OCFS2 */
+	case 0xAAD7AAEA:  /* PANFS */
+	case 0x517B:      /* SMB */
+	case 0xBEEFDEAD:  /* SNFS */
+	case 0xFE534D42:  /* SMB2 */
+	case 0xBACBACBC:  /* VMHGFS */
+	case 0xA501FCF5:  /* VXFS */
+		return 1;
+	default:
+		break;
+	}
+
+	return 0;
+}
+
+static int find_mount(const char *path, const struct statvfs *fs,
+	struct mntent *entry)
+{
+	const char *const mounts = "/proc/mounts";
+	char *rp = real_pathdup(path, 1);
+	struct mntent *ment = NULL;
+	struct statvfs mntfs;
+	FILE *fp;
+	int found = 0;
+	int dlen, plen, flen = 0;
+
+	entry->mnt_fsname = NULL;
+	entry->mnt_dir = NULL;
+	entry->mnt_type = NULL;
+
+	fp = setmntent(mounts, "r");
+	if (!fp) {
+		free(rp);
+		error_errno(_("setmntent('%s') failed"), mounts);
+		return -1;
+	}
+
+	plen = strlen(rp);
+
+	/* read all the mount information and compare to path */
+	while ((ment = getmntent(fp)) != NULL) {
+		if (statvfs(ment->mnt_dir, &mntfs)) {
+			switch (errno) {
+			case EPERM:
+			case ESRCH:
+			case EACCES:
+				continue;
+			default:
+				error_errno(_("statvfs('%s') failed"), ment->mnt_dir);
+				endmntent(fp);
+				free(rp);
+				return -1;
+			}
+		}
+
+		/* is mount on the same filesystem and is a prefix of the path */
+		if ((fs->f_fsid == mntfs.f_fsid) &&
+			!strncmp(ment->mnt_dir, rp, strlen(ment->mnt_dir))) {
+			dlen = strlen(ment->mnt_dir);
+			if (dlen > plen)
+				continue;
+			/*
+			 * look for the longest prefix (including root)
+			 */
+			if (dlen > flen &&
+				((dlen == 1 && ment->mnt_dir[0] == '/') ||
+				 (!rp[dlen] || rp[dlen] == '/'))) {
+				flen = dlen;
+				found = 1;
+
+				/*
+				 * https://man7.org/linux/man-pages/man3/getmntent.3.html
+				 *
+				 * The pointer points to a static area of memory which is
+				 * overwritten by subsequent calls to getmntent().
+				 */
+				free(entry->mnt_fsname);
+				free(entry->mnt_dir);
+				free(entry->mnt_type);
+				entry->mnt_fsname = xstrdup(ment->mnt_fsname);
+				entry->mnt_dir = xstrdup(ment->mnt_dir);
+				entry->mnt_type = xstrdup(ment->mnt_type);
+			}
+		}
+	}
+	endmntent(fp);
+	free(rp);
+
+	if (!found)
+		return -1;
+
+	return 0;
+}
+
+int fsmonitor__get_fs_info(const char *path, struct fs_info *fs_info)
+{
+	struct mntent entry;
+	struct statvfs fs;
+
+	fs_info->is_remote = -1;
+	fs_info->typename = NULL;
+
+	if (statvfs(path, &fs))
+		return error_errno(_("statvfs('%s') failed"), path);
+
+	if (find_mount(path, &fs, &entry) < 0) {
+		free(entry.mnt_fsname);
+		free(entry.mnt_dir);
+		free(entry.mnt_type);
+		return -1;
+	}
+
+	trace_printf_key(&trace_fsmonitor,
+			 "statvfs('%s') [flags 0x%08lx] '%s' '%s'",
+			 path, fs.f_flag, entry.mnt_type, entry.mnt_fsname);
+
+	fs_info->is_remote = is_remote_fs(entry.mnt_dir);
+	fs_info->typename = xstrdup(entry.mnt_fsname);
+	free(entry.mnt_fsname);
+	free(entry.mnt_dir);
+	free(entry.mnt_type);
+
+	if (fs_info->is_remote < 0)
+		return -1;
+
+	trace_printf_key(&trace_fsmonitor,
+				"'%s' is_remote: %d",
+				path, fs_info->is_remote);
+
+	return 0;
+}
+
+int fsmonitor__is_fs_remote(const char *path)
+{
+	struct fs_info fs;
+
+	if (fsmonitor__get_fs_info(path, &fs)) {
+		free(fs.typename);
+		return -1;
+	}
+
+	free(fs.typename);
+
+	return fs.is_remote;
+}
+
+/*
+ * No-op for now.
+ */
+int fsmonitor__get_alias(const char *path, struct alias_info *info)
+{
+	return 0;
+}
+
+/*
+ * No-op for now.
+ */
+char *fsmonitor__resolve_alias(const char *path,
+	const struct alias_info *info)
+{
+	return NULL;
+}
Signature · 1 line
gitgitgadget
#84 lore raw

PATCH v5 4/6 fsmonitor: enable fsmonitor for Linux

In reply to Eric DeCosta via GitGitGadget

From: Eric DeCosta <[email protected]>

Update build to enable fsmonitor for Linux.

---
 config.mak.uname                    |  8 ++++++++
 contrib/buildsystems/CMakeLists.txt | 11 ++++++++++-
 2 files changed, 18 insertions(+), 1 deletion(-)
config.mak.uname +8 -0
index d63629fe807..5890d810463 100644
@@ -68,6 +68,14 @@ ifeq ($(uname_S),Linux)
 	ifneq ($(findstring .el7.,$(uname_R)),)
 		BASIC_CFLAGS += -std=c99
 	endif
+	# The builtin FSMonitor on Linux builds upon Simple-IPC.  Both require
+	# Unix domain sockets and PThreads.
+	ifndef NO_PTHREADS
+	ifndef NO_UNIX_SOCKETS
+	FSMONITOR_DAEMON_BACKEND = linux
+	FSMONITOR_OS_SETTINGS = linux
+	endif
+	endif
 endif
 ifeq ($(uname_S),GNU/kFreeBSD)
 	HAVE_ALLOCA_H = YesPlease
contrib/buildsystems/CMakeLists.txt +10 -1
index 2f6e0197ffa..a0319527279 100644
@@ -304,7 +304,16 @@ else()
 endif()
 
 if(SUPPORTS_SIMPLE_IPC)
-	if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
+	if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
+		add_compile_definitions(HAVE_FSMONITOR_DAEMON_BACKEND)
+		list(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-linux.c)
+		list(APPEND compat_SOURCES compat/fsmonitor/fsm-health-linux.c)
+		list(APPEND compat_SOURCES compat/fsmonitor/fsm-ipc-linux.c)
+		list(APPEND compat_SOURCES compat/fsmonitor/fsm-path-utils-linux.c)
+
+		add_compile_definitions(HAVE_FSMONITOR_OS_SETTINGS)
+		list(APPEND compat_SOURCES compat/fsmonitor/fsm-settings-linux.c)
+	elseif(CMAKE_SYSTEM_NAME STREQUAL "Windows")
 		add_compile_definitions(HAVE_FSMONITOR_DAEMON_BACKEND)
 		list(APPEND compat_SOURCES compat/fsmonitor/fsm-listen-win32.c)
 		list(APPEND compat_SOURCES compat/fsmonitor/fsm-health-win32.c)
Signature · 1 line
gitgitgadget
#83 lore raw

PATCH v5 3/6 fsmonitor: implement filesystem change listener for Linux

In reply to Eric DeCosta via GitGitGadget

From: Eric DeCosta <[email protected]>

Implement a filesystem change listener for Linux based on the inotify API:
https://man7.org/linux/man-pages/man7/inotify.7.html

inotify requires registering a watch on every directory in the worktree and special handling of moves/renames.

---
 compat/fsmonitor/fsm-listen-linux.c | 676 ++++++++++++++++++++++++++++
 1 file changed, 676 insertions(+)
 create mode 100644 compat/fsmonitor/fsm-listen-linux.c
compat/fsmonitor/fsm-listen-linux.c new file +676 -0
new file mode 100644
index 00000000000..e8548e4e009
@@ -0,0 +1,676 @@
+#include "cache.h"
+#include "fsmonitor.h"
+#include "fsm-listen.h"
+#include "fsmonitor--daemon.h"
+#include <dirent.h>
+#include <fcntl.h>
+#include <sys/inotify.h>
+#include <sys/stat.h>
+
+/*
+ * Safe value to bitwise OR with rest of mask for
+ * kernels that do not support IN_MASK_CREATE
+ */
+#ifndef IN_MASK_CREATE
+#define IN_MASK_CREATE 0x00000000
+#endif
+
+enum shutdown_reason {
+	SHUTDOWN_CONTINUE = 0,
+	SHUTDOWN_STOP,
+	SHUTDOWN_ERROR,
+	SHUTDOWN_FORCE
+};
+
+struct watch_entry {
+	struct hashmap_entry ent;
+	int wd;
+	uint32_t cookie;
+	const char *dir;
+};
+
+struct rename_entry {
+	struct hashmap_entry ent;
+	time_t whence;
+	uint32_t cookie;
+	const char *dir;
+};
+
+struct fsm_listen_data {
+	int fd_inotify;
+	enum shutdown_reason shutdown;
+	struct hashmap watches;
+	struct hashmap renames;
+	struct hashmap revwatches;
+};
+
+static int watch_entry_cmp(const void *cmp_data,
+			  const struct hashmap_entry *eptr,
+			  const struct hashmap_entry *entry_or_key,
+			  const void *keydata)
+{
+	const struct watch_entry *e1, *e2;
+
+	e1 = container_of(eptr, const struct watch_entry, ent);
+	e2 = container_of(eptr, const struct watch_entry, ent);
+	return e1->wd != e2->wd;
+}
+
+static int revwatches_entry_cmp(const void *cmp_data,
+			  const struct hashmap_entry *eptr,
+			  const struct hashmap_entry *entry_or_key,
+			  const void *keydata)
+{
+	const struct watch_entry *e1, *e2;
+
+	e1 = container_of(eptr, const struct watch_entry, ent);
+	e2 = container_of(eptr, const struct watch_entry, ent);
+	return strcmp(e1->dir, e2->dir);
+}
+
+static int rename_entry_cmp(const void *cmp_data,
+			  const struct hashmap_entry *eptr,
+			  const struct hashmap_entry *entry_or_key,
+			  const void *keydata)
+{
+	const struct rename_entry *e1, *e2;
+
+	e1 = container_of(eptr, const struct rename_entry, ent);
+	e2 = container_of(eptr, const struct rename_entry, ent);
+	return e1->cookie != e2->cookie;
+}
+
+/*
+ * Register an inotify watch, add watch descriptor to path mapping
+ * and the reverse mapping.
+ */
+static int add_watch(const char *path, struct fsm_listen_data *data)
+{
+	const char *interned = strintern(path);
+	struct watch_entry *w1, *w2;
+
+	/* add the inotify watch, don't allow watches to be modified */
+	int wd = inotify_add_watch(data->fd_inotify, interned,
+				(IN_ALL_EVENTS | IN_ONLYDIR | IN_MASK_CREATE)
+				^ IN_ACCESS ^ IN_CLOSE ^ IN_OPEN);
+	if (wd < 0)
+		return error_errno("inotify_add_watch('%s') failed", interned);
+
+	/* add watch descriptor -> directory mapping */
+	CALLOC_ARRAY(w1, 1);
+	w1->wd = wd;
+	w1->dir = interned;
+	hashmap_entry_init(&w1->ent, memhash(&w1->wd, sizeof(int)));
+	hashmap_add(&data->watches, &w1->ent);
+
+	/* add directory -> watch descriptor mapping */
+	CALLOC_ARRAY(w2, 1);
+	w2->wd = wd;
+	w2->dir = interned;
+	hashmap_entry_init(&w2->ent, memhash(w2->dir, strlen(w2->dir)));
+	hashmap_add(&data->revwatches, &w2->ent);
+
+	return 0;
+}
+
+/*
+ * Remove the inotify watch, the watch descriptor to path mapping
+ * and the reverse mapping.
+ */
+static void remove_watch(struct watch_entry *w,
+	struct fsm_listen_data *data)
+{
+	struct watch_entry k1, k2, *w1, *w2;
+
+	/* remove watch, ignore error if kernel already did it */
+	if (inotify_rm_watch(data->fd_inotify, w->wd) && errno != EINVAL)
+		error_errno("inotify_rm_watch() failed");
+
+	hashmap_entry_init(&k1.ent, memhash(&w->wd, sizeof(int)));
+	w1 = hashmap_remove_entry(&data->watches, &k1, ent, NULL);
+	if (!w1)
+		BUG("Double remove of watch for '%s'", w->dir);
+
+	if (w1->cookie)
+		BUG("Removing watch for '%s' which has a pending rename", w1->dir);
+
+	hashmap_entry_init(&k2.ent, memhash(w->dir, strlen(w->dir)));
+	w2 = hashmap_remove_entry(&data->revwatches, &k2, ent, NULL);
+	if (!w2)
+		BUG("Double remove of reverse watch for '%s'", w->dir);
+
+	/* w1->dir and w2->dir are interned strings, we don't own them */
+	free(w1);
+	free(w2);
+}
+
+/*
+ * Check for stale directory renames.
+ *
+ * https://man7.org/linux/man-pages/man7/inotify.7.html
+ *
+ * Allow for some small timeout to account for the fact that insertion of the
+ * IN_MOVED_FROM+IN_MOVED_TO event pair is not atomic, and the possibility that
+ * there may not be any IN_MOVED_TO event.
+ *
+ * If the IN_MOVED_TO event is not received within the timeout then events have
+ * been missed and the monitor is in an inconsistent state with respect to the
+ * filesystem.
+ */
+static int check_stale_dir_renames(struct hashmap *renames, time_t max_age)
+{
+	struct rename_entry *re;
+	struct hashmap_iter iter;
+
+	hashmap_for_each_entry(renames, &iter, re, ent) {
+		if (re->whence <= max_age)
+			return -1;
+	}
+	return 0;
+}
+
+/*
+ * Track pending renames.
+ *
+ * Tracking is done via a event cookie to watch descriptor mapping.
+ *
+ * A rename is not complete until matching a IN_MOVED_TO event is received
+ * for a corresponding IN_MOVED_FROM event.
+ */
+static void add_dir_rename(uint32_t cookie, const char *path,
+	struct fsm_listen_data *data)
+{
+	struct watch_entry k, *w;
+	struct rename_entry *re;
+
+	/* lookup the watch descriptor for the given path */
+	hashmap_entry_init(&k.ent, memhash(path, strlen(path)));
+	w = hashmap_get_entry(&data->revwatches, &k, ent, NULL);
+	if (!w) /* should never happen */
+		BUG("No watch for '%s'", path);
+	w->cookie = cookie;
+
+	/* add the pending rename to match against later */
+	CALLOC_ARRAY(re, 1);
+	re->dir = w->dir;
+	re->cookie = w->cookie;
+	re->whence = time(NULL);
+	hashmap_entry_init(&re->ent, memhash(&re->cookie, sizeof(uint32_t)));
+	hashmap_add(&data->renames, &re->ent);
+}
+
+/*
+ * Handle directory renames
+ *
+ * Once a IN_MOVED_TO event is received, lookup the rename tracking information
+ * via the event cookie and use this information to update the watch.
+ */
+static void rename_dir(uint32_t cookie, const char *path,
+	struct fsm_listen_data *data)
+{
+	struct rename_entry rek, *re;
+	struct watch_entry k, *w;
+
+	/* lookup a pending rename to match */
+	rek.cookie = cookie;
+	hashmap_entry_init(&rek.ent, memhash(&rek.cookie, sizeof(uint32_t)));
+	re = hashmap_get_entry(&data->renames, &rek, ent, NULL);
+	if (re) {
+		k.dir = re->dir;
+		hashmap_entry_init(&k.ent, memhash(k.dir, strlen(k.dir)));
+		w = hashmap_get_entry(&data->revwatches, &k, ent, NULL);
+		if (w) {
+			w->cookie = 0; /* rename handled */
+			remove_watch(w, data);
+			add_watch(path, data);
+		} else {
+			BUG("No matching watch");
+		}
+	} else {
+		BUG("No matching cookie");
+	}
+}
+
+/*
+ * Recursively add watches to every directory under path
+ */
+static int register_inotify(const char *path,
+	struct fsmonitor_daemon_state *state,
+	struct fsmonitor_batch *batch)
+{
+	DIR *dir;
+	const char *rel;
+	struct strbuf current = STRBUF_INIT;
+	struct dirent *de;
+	struct stat fs;
+	int ret = -1;
+
+	dir = opendir(path);
+	if (!dir)
+		return error_errno("opendir('%s') failed", path);
+
+	while ((de = readdir_skip_dot_and_dotdot(dir)) != NULL) {
+		strbuf_reset(&current);
+		strbuf_addf(&current, "%s/%s", path, de->d_name);
+		if (lstat(current.buf, &fs)) {
+			error_errno("lstat('%s') failed", current.buf);
+			goto failed;
+		}
+
+		/* recurse into directory */
+		if (S_ISDIR(fs.st_mode)) {
+			if (add_watch(current.buf, state->listen_data))
+				goto failed;
+			if (register_inotify(current.buf, state, batch))
+				goto failed;
+		} else if (batch) {
+			rel = current.buf + state->path_worktree_watch.len + 1;
+			trace_printf_key(&trace_fsmonitor, "explicitly adding '%s'", rel);
+			fsmonitor_batch__add_path(batch, rel);
+		}
+	}
+	ret = 0;
+
+failed:
+	strbuf_release(&current);
+	if (closedir(dir) < 0)
+		return error_errno("closedir('%s') failed", path);
+	return ret;
+}
+
+static int em_rename_dir_from(u_int32_t mask)
+{
+	return ((mask & IN_ISDIR) && (mask & IN_MOVED_FROM));
+}
+
+static int em_rename_dir_to(u_int32_t mask)
+{
+	return ((mask & IN_ISDIR) && (mask & IN_MOVED_TO));
+}
+
+static int em_remove_watch(u_int32_t mask)
+{
+	return (mask & IN_DELETE_SELF);
+}
+
+static int em_dir_renamed(u_int32_t mask)
+{
+	return ((mask & IN_ISDIR) && (mask & IN_MOVE));
+}
+
+static int em_dir_created(u_int32_t mask)
+{
+	return ((mask & IN_ISDIR) && (mask & IN_CREATE));
+}
+
+static int em_dir_deleted(uint32_t mask)
+{
+	return ((mask & IN_ISDIR) && (mask & IN_DELETE));
+}
+
+static int em_force_shutdown(u_int32_t mask)
+{
+	return (mask & IN_UNMOUNT) || (mask & IN_Q_OVERFLOW);
+}
+
+static int em_ignore(u_int32_t mask)
+{
+	return (mask & IN_IGNORED) || (mask & IN_MOVE_SELF);
+}
+
+static void log_mask_set(const char *path, u_int32_t mask)
+{
+	struct strbuf msg = STRBUF_INIT;
+
+	if (mask & IN_ACCESS)
+		strbuf_addstr(&msg, "IN_ACCESS|");
+	if (mask & IN_MODIFY)
+		strbuf_addstr(&msg, "IN_MODIFY|");
+	if (mask & IN_ATTRIB)
+		strbuf_addstr(&msg, "IN_ATTRIB|");
+	if (mask & IN_CLOSE_WRITE)
+		strbuf_addstr(&msg, "IN_CLOSE_WRITE|");
+	if (mask & IN_CLOSE_NOWRITE)
+		strbuf_addstr(&msg, "IN_CLOSE_NOWRITE|");
+	if (mask & IN_OPEN)
+		strbuf_addstr(&msg, "IN_OPEN|");
+	if (mask & IN_MOVED_FROM)
+		strbuf_addstr(&msg, "IN_MOVED_FROM|");
+	if (mask & IN_MOVED_TO)
+		strbuf_addstr(&msg, "IN_MOVED_TO|");
+	if (mask & IN_CREATE)
+		strbuf_addstr(&msg, "IN_CREATE|");
+	if (mask & IN_DELETE)
+		strbuf_addstr(&msg, "IN_DELETE|");
+	if (mask & IN_DELETE_SELF)
+		strbuf_addstr(&msg, "IN_DELETE_SELF|");
+	if (mask & IN_MOVE_SELF)
+		strbuf_addstr(&msg, "IN_MOVE_SELF|");
+	if (mask & IN_UNMOUNT)
+		strbuf_addstr(&msg, "IN_UNMOUNT|");
+	if (mask & IN_Q_OVERFLOW)
+		strbuf_addstr(&msg, "IN_Q_OVERFLOW|");
+	if (mask & IN_IGNORED)
+		strbuf_addstr(&msg, "IN_IGNORED|");
+	if (mask & IN_ISDIR)
+		strbuf_addstr(&msg, "IN_ISDIR|");
+
+	trace_printf_key(&trace_fsmonitor, "inotify_event: '%s', mask=%#8.8x %s",
+				path, mask, msg.buf);
+
+	strbuf_release(&msg);
+}
+
+int fsm_listen__ctor(struct fsmonitor_daemon_state *state)
+{
+	int fd;
+	int ret = 0;
+	struct fsm_listen_data *data;
+
+	CALLOC_ARRAY(data, 1);
+	state->listen_data = data;
+	state->listen_error_code = -1;
+	data->shutdown = SHUTDOWN_ERROR;
+
+	fd = inotify_init1(O_NONBLOCK);
+	if (fd < 0)
+		return error_errno("inotify_init1() failed");
+
+	data->fd_inotify = fd;
+
+	hashmap_init(&data->watches, watch_entry_cmp, NULL, 0);
+	hashmap_init(&data->renames, rename_entry_cmp, NULL, 0);
+	hashmap_init(&data->revwatches, revwatches_entry_cmp, NULL, 0);
+
+	if (add_watch(state->path_worktree_watch.buf, data))
+		ret = -1;
+	else if (register_inotify(state->path_worktree_watch.buf, state, NULL))
+		ret = -1;
+	else if (state->nr_paths_watching > 1) {
+		if (add_watch(state->path_gitdir_watch.buf, data))
+			ret = -1;
+		else if (register_inotify(state->path_gitdir_watch.buf, state, NULL))
+			ret = -1;
+	}
+
+	if (!ret) {
+		state->listen_error_code = 0;
+		data->shutdown = SHUTDOWN_CONTINUE;
+	}
+
+	return ret;
+}
+
+void fsm_listen__dtor(struct fsmonitor_daemon_state *state)
+{
+	struct fsm_listen_data *data;
+	struct hashmap_iter iter;
+	struct watch_entry *w;
+	int fd;
+
+	if (!state || !state->listen_data)
+		return;
+
+	data = state->listen_data;
+	fd = data->fd_inotify;
+
+	hashmap_for_each_entry(&data->watches, &iter, w, ent) {
+		w->cookie = 0; /* ignore any pending renames */
+		remove_watch(w, data);
+	}
+	hashmap_clear(&data->watches);
+
+	hashmap_clear(&data->revwatches); /* remove_watch freed the entries */
+
+	hashmap_clear_and_free(&data->renames, struct rename_entry, ent);
+
+	FREE_AND_NULL(state->listen_data);
+
+	if (fd && (close(fd) < 0))
+		error_errno(_("closing inotify file descriptor failed"));
+}
+
+void fsm_listen__stop_async(struct fsmonitor_daemon_state *state)
+{
+	if (!state->listen_data->shutdown)
+		state->listen_data->shutdown = SHUTDOWN_STOP;
+}
+
+/*
+ * Process a single inotify event and queue for publication.
+ */
+static int process_event(const char *path,
+	const struct inotify_event *event,
+	struct fsmonitor_batch *batch,
+	struct string_list *cookie_list,
+	struct fsmonitor_daemon_state *state)
+{
+	const char *rel;
+	const char *last_sep;
+
+	switch (fsmonitor_classify_path_absolute(state, path)) {
+		case IS_INSIDE_DOT_GIT_WITH_COOKIE_PREFIX:
+		case IS_INSIDE_GITDIR_WITH_COOKIE_PREFIX:
+			/* Use just the filename of the cookie file. */
+			last_sep = find_last_dir_sep(path);
+			string_list_append(cookie_list,
+					last_sep ? last_sep + 1 : path);
+			break;
+		case IS_INSIDE_DOT_GIT:
+		case IS_INSIDE_GITDIR:
+			break;
+		case IS_DOT_GIT:
+		case IS_GITDIR:
+			/*
+			* If .git directory is deleted or renamed away,
+			* we have to quit.
+			*/
+			if (em_dir_deleted(event->mask)) {
+				trace_printf_key(&trace_fsmonitor,
+						"event: gitdir removed");
+				state->listen_data->shutdown = SHUTDOWN_FORCE;
+				goto done;
+			}
+
+			if (em_dir_renamed(event->mask)) {
+				trace_printf_key(&trace_fsmonitor,
+						"event: gitdir renamed");
+				state->listen_data->shutdown = SHUTDOWN_FORCE;
+				goto done;
+			}
+			break;
+		case IS_WORKDIR_PATH:
+			/* normal events in the working directory */
+			if (trace_pass_fl(&trace_fsmonitor))
+				log_mask_set(path, event->mask);
+
+			rel = path + state->path_worktree_watch.len + 1;
+			fsmonitor_batch__add_path(batch, rel);
+
+			if (em_dir_deleted(event->mask))
+				break;
+
+			/* received IN_MOVE_FROM, add tracking for expected IN_MOVE_TO */
+			if (em_rename_dir_from(event->mask))
+				add_dir_rename(event->cookie, path, state->listen_data);
+
+			/* received IN_MOVE_TO, update watch to reflect new path */
+			if (em_rename_dir_to(event->mask)) {
+				rename_dir(event->cookie, path, state->listen_data);
+				if (register_inotify(path, state, batch)) {
+					state->listen_data->shutdown = SHUTDOWN_ERROR;
+					goto done;
+				}
+			}
+
+			if (em_dir_created(event->mask)) {
+				if (add_watch(path, state->listen_data)) {
+					state->listen_data->shutdown = SHUTDOWN_ERROR;
+					goto done;
+				}
+				if (register_inotify(path, state, batch)) {
+					state->listen_data->shutdown = SHUTDOWN_ERROR;
+					goto done;
+				}
+			}
+			break;
+		case IS_OUTSIDE_CONE:
+		default:
+			trace_printf_key(&trace_fsmonitor,
+					"ignoring '%s'", path);
+			break;
+	}
+	return 0;
+done:
+	return -1;
+}
+
+/*
+ * Read the inotify event stream and pre-process events before further
+ * processing and eventual publishing.
+ */
+static void handle_events(struct fsmonitor_daemon_state *state)
+{
+	 /* See https://man7.org/linux/man-pages/man7/inotify.7.html */
+	char buf[4096]
+		__attribute__ ((aligned(__alignof__(struct inotify_event))));
+
+	struct hashmap watches = state->listen_data->watches;
+	struct fsmonitor_batch *batch = NULL;
+	struct string_list cookie_list = STRING_LIST_INIT_DUP;
+	struct watch_entry k, *w;
+	struct strbuf path;
+	const struct inotify_event *event;
+	int fd = state->listen_data->fd_inotify;
+	ssize_t len;
+	char *ptr, *p;
+
+	strbuf_init(&path, PATH_MAX);
+
+	for(;;) {
+		len = read(fd, buf, sizeof(buf));
+		if (len == -1 && errno != EAGAIN) {
+			error_errno(_("reading inotify message stream failed"));
+			state->listen_data->shutdown = SHUTDOWN_ERROR;
+			goto done;
+		}
+
+		/* nothing to read */
+		if (len <= 0)
+			goto done;
+
+		/* Loop over all events in the buffer. */
+		for (ptr = buf; ptr < buf + len;
+			 ptr += sizeof(struct inotify_event) + event->len) {
+
+			event = (const struct inotify_event *) ptr;
+
+			if (em_ignore(event->mask))
+				continue;
+
+			/* File system was unmounted or event queue overflowed */
+			if (em_force_shutdown(event->mask)) {
+				if (trace_pass_fl(&trace_fsmonitor))
+					log_mask_set("Forcing shutdown", event->mask);
+				state->listen_data->shutdown = SHUTDOWN_FORCE;
+				goto done;
+			}
+
+			hashmap_entry_init(&k.ent, memhash(&event->wd, sizeof(int)));
+			k.wd = event->wd;
+
+			w = hashmap_get_entry(&watches, &k, ent, NULL);
+			if (!w) /* should never happen */
+				BUG("No watch for '%s'", event->name);
+
+			/* directory watch was removed */
+			if (em_remove_watch(event->mask)) {
+				remove_watch(w, state->listen_data);
+				continue;
+			}
+
+			strbuf_reset(&path);
+			strbuf_add(&path, w->dir, strlen(w->dir));
+			strbuf_addch(&path, '/');
+			strbuf_addstr(&path, event->name);
+
+			p = fsmonitor__resolve_alias(path.buf, &state->alias);
+			if (!p)
+				p = strbuf_detach(&path, NULL);
+
+			if (!batch)
+				batch = fsmonitor_batch__new();
+
+			if (process_event(p, event, batch, &cookie_list, state)) {
+				free(p);
+				goto done;
+			}
+			free(p);
+		}
+		strbuf_reset(&path);
+		fsmonitor_publish(state, batch, &cookie_list);
+		string_list_clear(&cookie_list, 0);
+		batch = NULL;
+	}
+done:
+	strbuf_release(&path);
+	fsmonitor_batch__free_list(batch);
+	string_list_clear(&cookie_list, 0);
+}
+
+/*
+ * Non-blocking read of the inotify events stream. The inotify fd is polled
+ * frequently to help minimize the number of queue overflows.
+ */
+void fsm_listen__loop(struct fsmonitor_daemon_state *state)
+{
+	int poll_num;
+	const int interval = 1000;
+	time_t checked = time(NULL);
+	struct pollfd fds[1];
+	fds[0].fd = state->listen_data->fd_inotify;
+	fds[0].events = POLLIN;
+
+	for(;;) {
+		switch (state->listen_data->shutdown) {
+			case SHUTDOWN_CONTINUE:
+				poll_num = poll(fds, 1, 1);
+				if (poll_num == -1) {
+					if (errno == EINTR)
+						continue;
+					error_errno(_("polling inotify message stream failed"));
+					state->listen_data->shutdown = SHUTDOWN_ERROR;
+					continue;
+				}
+
+				if ((time(NULL) - checked) >= interval) {
+					checked = time(NULL);
+					if (check_stale_dir_renames(&state->listen_data->renames,
+						checked - interval)) {
+						trace_printf_key(&trace_fsmonitor,
+							"Missed IN_MOVED_TO events, forcing shutdown");
+						state->listen_data->shutdown = SHUTDOWN_FORCE;
+						continue;
+					}
+				}
+
+				if (poll_num > 0 && (fds[0].revents & POLLIN))
+					handle_events(state);
+
+				continue;
+			case SHUTDOWN_ERROR:
+				state->listen_error_code = -1;
+				ipc_server_stop_async(state->ipc_server_data);
+				break;
+			case SHUTDOWN_FORCE:
+				state->listen_error_code = 0;
+				ipc_server_stop_async(state->ipc_server_data);
+				break;
+			case SHUTDOWN_STOP:
+			default:
+				state->listen_error_code = 0;
+				break;
+		}
+		return;
+	}
+}
Signature · 1 line
gitgitgadget
#85 lore raw

PATCH v5 5/6 fsmonitor: test updates

In reply to Eric DeCosta via GitGitGadget

From: Eric DeCosta <[email protected]>

t7527-builtin-fsmonitor was leaking fsmonitor--daemon processes in some cases.

Accomodate slight difference in the number of events generated on Linux.

On lower-powered systems, spin a little to give the daemon time to respond to and log filesystem events.

---
 t/t7527-builtin-fsmonitor.sh | 94 ++++++++++++++++++++++++++++++------
 1 file changed, 80 insertions(+), 14 deletions(-)
t/t7527-builtin-fsmonitor.sh +80 -14
index 4abc74db2bb..951374231b7 100755
@@ -13,7 +13,7 @@ fi
 stop_daemon_delete_repo () {
 	r=$1 &&
 	test_might_fail git -C $r fsmonitor--daemon stop &&
-	rm -rf $1
+	rm -rf $r
 }
 
 start_daemon () {
@@ -72,6 +72,34 @@ start_daemon () {
 	)
 }
 
+IMPLICIT_TIMEOUT=5
+
+wait_for_update () {
+	func=$1 &&
+	file=$2 &&
+	sz=$(wc -c < "$file") &&
+	last=0 &&
+	$func &&
+	k=0 &&
+	while test "$k" -lt $IMPLICIT_TIMEOUT
+	do
+		nsz=$(wc -c < "$file")
+		if test "$nsz" -gt "$sz"
+		then
+			if test "$last" -eq "$nsz"
+			then
+				cat "$file" &&
+				return 0
+			fi
+			last=$nsz
+		fi
+		sleep 1
+		k=$(( $k + 1 ))
+	done &&
+	cat "$file" &&
+	return 0
+}
+
 # Is a Trace2 data event present with the given catetory and key?
 # We do not care what the value is.
 #
@@ -137,7 +165,6 @@ test_expect_success 'implicit daemon start' '
 # machines (where it might take a moment to wake and reschedule the
 # daemon process) to avoid false alarms during test runs.)
 #
-IMPLICIT_TIMEOUT=5
 
 verify_implicit_shutdown () {
 	r=$1 &&
@@ -373,6 +400,15 @@ create_files () {
 	echo 3 >dir2/new
 }
 
+rename_directory () {
+	mv dirtorename dirrenamed
+}
+
+rename_directory_file () {
+	mv dirtorename dirrenamed &&
+	echo 1 > dirrenamed/new
+}
+
 rename_files () {
 	mv rename renamed &&
 	mv dir1/rename dir1/renamed &&
@@ -427,10 +463,12 @@ test_expect_success 'edit some files' '
 
 	start_daemon --tf "$PWD/.git/trace" &&
 
-	edit_files &&
+	wait_for_update edit_files "$PWD/.git/trace" &&
 
 	test-tool fsmonitor-client query --token 0 &&
 
+	test_might_fail git fsmonitor--daemon stop &&
+
 	grep "^event: dir1/modified$"  .git/trace &&
 	grep "^event: dir2/modified$"  .git/trace &&
 	grep "^event: modified$"       .git/trace &&
@@ -442,10 +480,12 @@ test_expect_success 'create some files' '
 
 	start_daemon --tf "$PWD/.git/trace" &&
 
-	create_files &&
+	wait_for_update create_files "$PWD/.git/trace" &&
 
 	test-tool fsmonitor-client query --token 0 &&
 
+	test_might_fail git fsmonitor--daemon stop &&
+
 	grep "^event: dir1/new$" .git/trace &&
 	grep "^event: dir2/new$" .git/trace &&
 	grep "^event: new$"      .git/trace
@@ -456,10 +496,12 @@ test_expect_success 'delete some files' '
 
 	start_daemon --tf "$PWD/.git/trace" &&
 
-	delete_files &&
+	wait_for_update delete_files "$PWD/.git/trace" &&
 
 	test-tool fsmonitor-client query --token 0 &&
 
+	test_might_fail git fsmonitor--daemon stop &&
+
 	grep "^event: dir1/delete$" .git/trace &&
 	grep "^event: dir2/delete$" .git/trace &&
 	grep "^event: delete$"      .git/trace
@@ -470,10 +512,12 @@ test_expect_success 'rename some files' '
 
 	start_daemon --tf "$PWD/.git/trace" &&
 
-	rename_files &&
+	wait_for_update rename_files "$PWD/.git/trace" &&
 
 	test-tool fsmonitor-client query --token 0 &&
 
+	test_might_fail git fsmonitor--daemon stop &&
+
 	grep "^event: dir1/rename$"  .git/trace &&
 	grep "^event: dir2/rename$"  .git/trace &&
 	grep "^event: rename$"       .git/trace &&
@@ -487,23 +531,42 @@ test_expect_success 'rename directory' '
 
 	start_daemon --tf "$PWD/.git/trace" &&
 
-	mv dirtorename dirrenamed &&
+	wait_for_update rename_directory "$PWD/.git/trace" &&
 
 	test-tool fsmonitor-client query --token 0 &&
 
+	test_might_fail git fsmonitor--daemon stop &&
+
 	grep "^event: dirtorename/*$" .git/trace &&
 	grep "^event: dirrenamed/*$"  .git/trace
 '
 
+test_expect_success 'rename directory file' '
+	test_when_finished clean_up_repo_and_stop_daemon &&
+
+	start_daemon --tf "$PWD/.git/trace" &&
+
+	wait_for_update rename_directory_file "$PWD/.git/trace" &&
+
+	test-tool fsmonitor-client query --token 0 &&
+
+	test_might_fail git fsmonitor--daemon stop &&
+
+	grep "^event: dirtorename/*$" .git/trace &&
+	grep "^event: dirrenamed/*$"  .git/trace &&
+	grep "^event: dirrenamed/new$"  .git/trace
+'
 test_expect_success 'file changes to directory' '
 	test_when_finished clean_up_repo_and_stop_daemon &&
 
 	start_daemon --tf "$PWD/.git/trace" &&
 
-	file_to_directory &&
+	wait_for_update file_to_directory "$PWD/.git/trace" &&
 
 	test-tool fsmonitor-client query --token 0 &&
 
+	test_might_fail git fsmonitor--daemon stop &&
+
 	grep "^event: delete$"     .git/trace &&
 	grep "^event: delete/new$" .git/trace
 '
@@ -513,10 +576,12 @@ test_expect_success 'directory changes to a file' '
 
 	start_daemon --tf "$PWD/.git/trace" &&
 
-	directory_to_file &&
+	wait_for_update directory_to_file "$PWD/.git/trace" &&
 
 	test-tool fsmonitor-client query --token 0 &&
 
+	test_might_fail git fsmonitor--daemon stop &&
+
 	grep "^event: dir1$" .git/trace
 '
 
@@ -561,7 +626,7 @@ test_expect_success 'flush cached data' '
 	test-tool -C test_flush fsmonitor-client query --token "builtin:test_00000002:0" >actual_2 &&
 	nul_to_q <actual_2 >actual_q2 &&
 
-	grep "^builtin:test_00000002:0Q$" actual_q2 &&
+	grep "^builtin:test_00000002:[0-1]Q$" actual_q2 &&
 
 	>test_flush/file_3 &&
 
@@ -732,7 +797,8 @@ u_values="$u1 $u2"
 for u in $u_values
 do
 	test_expect_success "unicode in repo root path: $u" '
-		test_when_finished "stop_daemon_delete_repo $u" &&
+		test_when_finished \
+		"stop_daemon_delete_repo `echo "$u" | sed 's:x:\\\\\\\\\\\\\\x:g'`" &&
 
 		git init "$u" &&
 		echo 1 >"$u"/file1 &&
@@ -818,8 +884,7 @@ test_expect_success 'submodule setup' '
 '
 
 test_expect_success 'submodule always visited' '
-	test_when_finished "git -C super fsmonitor--daemon stop; \
-			    rm -rf super; \
+	test_when_finished "rm -rf super; \
 			    rm -rf sub" &&
 
 	create_super super &&
@@ -887,7 +952,8 @@ have_t2_error_event () {
 }
 
 test_expect_success "stray submodule super-prefix warning" '
-	test_when_finished "rm -rf super; \
+	test_when_finished "git -C super/dir_1/dir_2/sub fsmonitor--daemon stop; \
+			    rm -rf super; \
 			    rm -rf sub;   \
 			    rm super-sub.trace" &&
 
Signature · 1 line
gitgitgadget
#86 lore raw

PATCH v5 6/6 fsmonitor: update doc for Linux

In reply to Eric DeCosta via GitGitGadget

From: Eric DeCosta <[email protected]>

Update the documentation for Linux.

---
 Documentation/config/fsmonitor--daemon.txt |  4 ++--
 Documentation/git-fsmonitor--daemon.txt    | 24 ++++++++++++++--------
 2 files changed, 18 insertions(+), 10 deletions(-)
Documentation/config/fsmonitor--daemon.txt +2 -2
index c225c6c9e74..2cafb040d96 100644
@@ -4,8 +4,8 @@ fsmonitor.allowRemote::
     behavior.  Only respected when `core.fsmonitor` is set to `true`.
 
 fsmonitor.socketDir::
-    This Mac OS-specific option, if set, specifies the directory in
+    Mac OS and Linux-specific option. If set, specifies the directory in
     which to create the Unix domain socket used for communication
     between the fsmonitor daemon and various Git commands. The directory must
-    reside on a native Mac OS filesystem.  Only respected when `core.fsmonitor`
+    reside on a native filesystem.  Only respected when `core.fsmonitor`
     is set to `true`.
Documentation/git-fsmonitor--daemon.txt +16 -8
index 8238eadb0e1..c2b08229c74 100644
@@ -76,23 +76,31 @@ repositories; this may be overridden by setting `fsmonitor.allowRemote` to
 correctly with all network-mounted repositories and such use is considered
 experimental.
 
-On Mac OS, the inter-process communication (IPC) between various Git
+On Linux and Mac OS, the inter-process communication (IPC) between various Git
 commands and the fsmonitor daemon is done via a Unix domain socket (UDS) -- a
-special type of file -- which is supported by native Mac OS filesystems,
-but not on network-mounted filesystems, NTFS, or FAT32.  Other filesystems
-may or may not have the needed support; the fsmonitor daemon is not guaranteed
-to work with these filesystems and such use is considered experimental.
+special type of file -- which is supported by many native Linux and Mac OS
+filesystems, but not on network-mounted filesystems, NTFS, or FAT32.  Other
+filesystems may or may not have the needed support; the fsmonitor daemon is not
+guaranteed to work with these filesystems and such use is considered
+experimental.
 
 By default, the socket is created in the `.git` directory, however, if the
 `.git` directory is on a network-mounted filesystem, it will be instead be
 created at `$HOME/.git-fsmonitor-*` unless `$HOME` itself is on a
 network-mounted filesystem in which case you must set the configuration
-variable `fsmonitor.socketDir` to the path of a directory on a Mac OS native
+variable `fsmonitor.socketDir` to the path of a directory on a native
 filesystem in which to create the socket file.
 
 If none of the above directories (`.git`, `$HOME`, or `fsmonitor.socketDir`)
-is on a native Mac OS file filesystem the fsmonitor daemon will report an
-error that will cause the daemon and the currently running command to exit.
+is on a native Linux or Mac OS filesystem the fsmonitor daemon will report
+an error that will cause the daemon to exit and the currently running command
+to issue a warning.
+
+On Linux, the fsmonitor daemon registers a watch for each directory in the
+repository.  The default per-user limit for the number of watches on most Linux
+systems is 8192.  This may not be sufficient for large repositories or if
+multiple instances of the fsmonitor daemon are running.
+See https://watchexec.github.io/docs/inotify-limits.html[Linux inotify limits] for more information.
 
 CONFIGURATION
 -------------
Signature · 1 line
gitgitgadget
#87 lore raw
In reply to Eric DeCosta via GitGitGadget
Quoted 10 lines · Eric DeCosta via GitGitGadget

Goal is to deliver fsmonitor for Linux that is on par with fsmonitor for Windows and Mac OS.

This patch set builds upon previous work for done for Windows and Mac OS to implement a fsmonitor back-end for Linux based on the Linux inotify API. inotify differs significantly from the equivalent Windows and Mac OS APIs in that a watch must be registered for every directory of interest (rather than a singular watch at the root of the directory tree) and special care must be taken to handle directory renames correctly.

Quoted 6 lines

More information about inotify:
https://man7.org/linux/man-pages/man7/inotify.7.html

v4 differs from v3:

* Code review feedback

This has been dormant for full four months, and it seems to break linux-asan CI job when merged to 'seen',

https://github.com/git/git/actions/runs/4672116751/jobs/8273938089

while the same 'seen' excluding this topic passes everything.

https://github.com/git/git/actions/runs/4674694371

For now, I'll drop this topic from 'seen'.

Thanks.