fsmonitor--daemon: implement 'run' command

Implement `run` command to try to begin listening for file system events. This version defines the thread structure with a single fsmonitor_fs_listen thread to watch for file system events and a simple IPC thread pool to watch for connection from Git clients over a well-known named pipe or Unix domain socket. This commit does not actually do anything yet because the platform backends are still just stubs. Signed-off-by: Jeff Hostetler <jeffhost@microsoft.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Jeff Hostetler committed Mar 25, 2022 at 18:02 UTC 9dcba0ba08d0e843eda1f141c99c72e0aa67811f
2 files changed +261 -1
builtin/fsmonitor--daemon.c
+227 -1
@@ -3,16 +3,52 @@
3 #include "parse-options.h"
4 #include "fsmonitor.h"
5 #include "fsmonitor-ipc.h"
6 +#include "compat/fsmonitor/fsm-listen.h"
7 +#include "fsmonitor--daemon.h"
8 #include "simple-ipc.h"
9 #include "khash.h"
10
11 static const char * const builtin_fsmonitor__daemon_usage[] = {
12 + N_("git fsmonitor--daemon run [<options>]"),
13 N_("git fsmonitor--daemon stop"),
14 N_("git fsmonitor--daemon status"),
15 NULL
16 };
17
18 #ifdef HAVE_FSMONITOR_DAEMON_BACKEND
19 +/*
20 + * Global state loaded from config.
21 + */
22 +#define FSMONITOR__IPC_THREADS "fsmonitor.ipcthreads"
23 +static int fsmonitor__ipc_threads = 8;
24 +
25 +#define FSMONITOR__ANNOUNCE_STARTUP "fsmonitor.announcestartup"
26 +static int fsmonitor__announce_startup = 0;
27 +
28 +static int fsmonitor_config(const char *var, const char *value, void *cb)
29 +{
30 + if (!strcmp(var, FSMONITOR__IPC_THREADS)) {
31 + int i = git_config_int(var, value);
32 + if (i < 1)
33 + return error(_("value of '%s' out of range: %d"),
34 + FSMONITOR__IPC_THREADS, i);
35 + fsmonitor__ipc_threads = i;
36 + return 0;
37 + }
38 +
39 + if (!strcmp(var, FSMONITOR__ANNOUNCE_STARTUP)) {
40 + int is_bool;
41 + int i = git_config_bool_or_int(var, value, &is_bool);
42 + if (i < 0)
43 + return error(_("value of '%s' not bool or int: %d"),
44 + var, i);
45 + fsmonitor__announce_startup = i;
46 + return 0;
47 + }
48 +
49 + return git_default_config(var, value, cb);
50 +}
51 +
52 /*
53 * Acting as a CLIENT.
54 *
@@ -57,15 +93,198 @@ static int do_as_client__status(void)
93 }
94 }
95
96 +static ipc_server_application_cb handle_client;
97 +
98 +static int handle_client(void *data,
99 + const char *command, size_t command_len,
100 + ipc_server_reply_cb *reply,
101 + struct ipc_server_reply_data *reply_data)
102 +{
103 + /* struct fsmonitor_daemon_state *state = data; */
104 + int result;
105 +
106 + /*
107 + * The Simple IPC API now supports {char*, len} arguments, but
108 + * FSMonitor always uses proper null-terminated strings, so
109 + * we can ignore the command_len argument. (Trust, but verify.)
110 + */
111 + if (command_len != strlen(command))
112 + BUG("FSMonitor assumes text messages");
113 +
114 + trace2_region_enter("fsmonitor", "handle_client", the_repository);
115 + trace2_data_string("fsmonitor", the_repository, "request", command);
116 +
117 + result = 0; /* TODO Do something here. */
118 +
119 + trace2_region_leave("fsmonitor", "handle_client", the_repository);
120 +
121 + return result;
122 +}
123 +
124 +static void *fsm_listen__thread_proc(void *_state)
125 +{
126 + struct fsmonitor_daemon_state *state = _state;
127 +
128 + trace2_thread_start("fsm-listen");
129 +
130 + trace_printf_key(&trace_fsmonitor, "Watching: worktree '%s'",
131 + state->path_worktree_watch.buf);
132 + if (state->nr_paths_watching > 1)
133 + trace_printf_key(&trace_fsmonitor, "Watching: gitdir '%s'",
134 + state->path_gitdir_watch.buf);
135 +
136 + fsm_listen__loop(state);
137 +
138 + trace2_thread_exit();
139 + return NULL;
140 +}
141 +
142 +static int fsmonitor_run_daemon_1(struct fsmonitor_daemon_state *state)
143 +{
144 + struct ipc_server_opts ipc_opts = {
145 + .nr_threads = fsmonitor__ipc_threads,
146 +
147 + /*
148 + * We know that there are no other active threads yet,
149 + * so we can let the IPC layer temporarily chdir() if
150 + * it needs to when creating the server side of the
151 + * Unix domain socket.
152 + */
153 + .uds_disallow_chdir = 0
154 + };
155 +
156 + /*
157 + * Start the IPC thread pool before the we've started the file
158 + * system event listener thread so that we have the IPC handle
159 + * before we need it.
160 + */
161 + if (ipc_server_run_async(&state->ipc_server_data,
162 + fsmonitor_ipc__get_path(), &ipc_opts,
163 + handle_client, state))
164 + return error_errno(
165 + _("could not start IPC thread pool on '%s'"),
166 + fsmonitor_ipc__get_path());
167 +
168 + /*
169 + * Start the fsmonitor listener thread to collect filesystem
170 + * events.
171 + */
172 + if (pthread_create(&state->listener_thread, NULL,
173 + fsm_listen__thread_proc, state) < 0) {
174 + ipc_server_stop_async(state->ipc_server_data);
175 + ipc_server_await(state->ipc_server_data);
176 +
177 + return error(_("could not start fsmonitor listener thread"));
178 + }
179 +
180 + /*
181 + * The daemon is now fully functional in background threads.
182 + * Wait for the IPC thread pool to shutdown (whether by client
183 + * request or from filesystem activity).
184 + */
185 + ipc_server_await(state->ipc_server_data);
186 +
187 + /*
188 + * The fsmonitor listener thread may have received a shutdown
189 + * event from the IPC thread pool, but it doesn't hurt to tell
190 + * it again. And wait for it to shutdown.
191 + */
192 + fsm_listen__stop_async(state);
193 + pthread_join(state->listener_thread, NULL);
194 +
195 + return state->error_code;
196 +}
197 +
198 +static int fsmonitor_run_daemon(void)
199 +{
200 + struct fsmonitor_daemon_state state;
201 + int err;
202 +
203 + memset(&state, 0, sizeof(state));
204 +
205 + pthread_mutex_init(&state.main_lock, NULL);
206 + state.error_code = 0;
207 + state.current_token_data = NULL;
208 +
209 + /* Prepare to (recursively) watch the <worktree-root> directory. */
210 + strbuf_init(&state.path_worktree_watch, 0);
211 + strbuf_addstr(&state.path_worktree_watch, absolute_path(get_git_work_tree()));
212 + state.nr_paths_watching = 1;
213 +
214 + /*
215 + * We create and delete cookie files somewhere inside the .git
216 + * directory to help us keep sync with the file system. If
217 + * ".git" is not a directory, then <gitdir> is not inside the
218 + * cone of <worktree-root>, so set up a second watch to watch
219 + * the <gitdir> so that we get events for the cookie files.
220 + */
221 + strbuf_init(&state.path_gitdir_watch, 0);
222 + strbuf_addbuf(&state.path_gitdir_watch, &state.path_worktree_watch);
223 + strbuf_addstr(&state.path_gitdir_watch, "/.git");
224 + if (!is_directory(state.path_gitdir_watch.buf)) {
225 + strbuf_reset(&state.path_gitdir_watch);
226 + strbuf_addstr(&state.path_gitdir_watch, absolute_path(get_git_dir()));
227 + state.nr_paths_watching = 2;
228 + }
229 +
230 + /*
231 + * Confirm that we can create platform-specific resources for the
232 + * filesystem listener before we bother starting all the threads.
233 + */
234 + if (fsm_listen__ctor(&state)) {
235 + err = error(_("could not initialize listener thread"));
236 + goto done;
237 + }
238 +
239 + err = fsmonitor_run_daemon_1(&state);
240 +
241 +done:
242 + pthread_mutex_destroy(&state.main_lock);
243 + fsm_listen__dtor(&state);
244 +
245 + ipc_server_free(state.ipc_server_data);
246 +
247 + strbuf_release(&state.path_worktree_watch);
248 + strbuf_release(&state.path_gitdir_watch);
249 +
250 + return err;
251 +}
252 +
253 +static int try_to_run_foreground_daemon(void)
254 +{
255 + /*
256 + * Technically, we don't need to probe for an existing daemon
257 + * process, since we could just call `fsmonitor_run_daemon()`
258 + * and let it fail if the pipe/socket is busy.
259 + *
260 + * However, this method gives us a nicer error message for a
261 + * common error case.
262 + */
263 + if (fsmonitor_ipc__get_state() == IPC_STATE__LISTENING)
264 + die(_("fsmonitor--daemon is already running '%s'"),
265 + the_repository->worktree);
266 +
267 + if (fsmonitor__announce_startup) {
268 + fprintf(stderr, _("running fsmonitor-daemon in '%s'\n"),
269 + the_repository->worktree);
270 + fflush(stderr);
271 + }
272 +
273 + return !!fsmonitor_run_daemon();
274 +}
275 +
276 int cmd_fsmonitor__daemon(int argc, const char **argv, const char *prefix)
277 {
278 const char *subcmd;
279
280 struct option options[] = {
281 + OPT_INTEGER(0, "ipc-threads",
282 + &fsmonitor__ipc_threads,
283 + N_("use <n> ipc worker threads")),
284 OPT_END()
285 };
286
68 - git_config(git_default_config, NULL);
287 + git_config(fsmonitor_config, NULL);
288
289 argc = parse_options(argc, argv, prefix, options,
290 builtin_fsmonitor__daemon_usage, 0);
@@ -73,6 +292,13 @@ int cmd_fsmonitor__daemon(int argc, const char **argv, const char *prefix)
292 usage_with_options(builtin_fsmonitor__daemon_usage, options);
293 subcmd = argv[0];
294
295 + if (fsmonitor__ipc_threads < 1)
296 + die(_("invalid 'ipc-threads' value (%d)"),
297 + fsmonitor__ipc_threads);
298 +
299 + if (!strcmp(subcmd, "run"))
300 + return !!try_to_run_foreground_daemon();
301 +
302 if (!strcmp(subcmd, "stop"))
303 return !!do_as_client__send_stop();
304
fsmonitor--daemon.h new
+34
@@ -0,0 +1,34 @@
1 +#ifndef FSMONITOR_DAEMON_H
2 +#define FSMONITOR_DAEMON_H
3 +
4 +#ifdef HAVE_FSMONITOR_DAEMON_BACKEND
5 +
6 +#include "cache.h"
7 +#include "dir.h"
8 +#include "run-command.h"
9 +#include "simple-ipc.h"
10 +#include "thread-utils.h"
11 +
12 +struct fsmonitor_batch;
13 +struct fsmonitor_token_data;
14 +
15 +struct fsmonitor_daemon_backend_data; /* opaque platform-specific data */
16 +
17 +struct fsmonitor_daemon_state {
18 + pthread_t listener_thread;
19 + pthread_mutex_t main_lock;
20 +
21 + struct strbuf path_worktree_watch;
22 + struct strbuf path_gitdir_watch;
23 + int nr_paths_watching;
24 +
25 + struct fsmonitor_token_data *current_token_data;
26 +
27 + int error_code;
28 + struct fsmonitor_daemon_backend_data *backend_data;
29 +
30 + struct ipc_server_data *ipc_server_data;
31 +};
32 +
33 +#endif /* HAVE_FSMONITOR_DAEMON_BACKEND */
34 +#endif /* FSMONITOR_DAEMON_H */