#include "builtin.h" #include "config.h" #include "parse-options.h" #include "fsmonitor.h" #include "fsmonitor-ipc.h" #include "compat/fsmonitor/fsm-listen.h" #include "fsmonitor--daemon.h" #include "simple-ipc.h" #include "khash.h" static const char * const builtin_fsmonitor__daemon_usage[] = { N_("git fsmonitor--daemon start []"), N_("git fsmonitor--daemon run []"), N_("git fsmonitor--daemon stop"), N_("git fsmonitor--daemon status"), NULL }; #ifdef HAVE_FSMONITOR_DAEMON_BACKEND /* * Global state loaded from config. */ #define FSMONITOR__IPC_THREADS "fsmonitor.ipcthreads" static int fsmonitor__ipc_threads = 8; #define FSMONITOR__START_TIMEOUT "fsmonitor.starttimeout" static int fsmonitor__start_timeout_sec = 60; #define FSMONITOR__ANNOUNCE_STARTUP "fsmonitor.announcestartup" static int fsmonitor__announce_startup = 0; static int fsmonitor_config(const char *var, const char *value, void *cb) { if (!strcmp(var, FSMONITOR__IPC_THREADS)) { int i = git_config_int(var, value); if (i < 1) return error(_("value of '%s' out of range: %d"), FSMONITOR__IPC_THREADS, i); fsmonitor__ipc_threads = i; return 0; } if (!strcmp(var, FSMONITOR__START_TIMEOUT)) { int i = git_config_int(var, value); if (i < 0) return error(_("value of '%s' out of range: %d"), FSMONITOR__START_TIMEOUT, i); fsmonitor__start_timeout_sec = i; return 0; } if (!strcmp(var, FSMONITOR__ANNOUNCE_STARTUP)) { int is_bool; int i = git_config_bool_or_int(var, value, &is_bool); if (i < 0) return error(_("value of '%s' not bool or int: %d"), var, i); fsmonitor__announce_startup = i; return 0; } return git_default_config(var, value, cb); } /* * Acting as a CLIENT. * * Send a "quit" command to the `git-fsmonitor--daemon` (if running) * and wait for it to shutdown. */ static int do_as_client__send_stop(void) { struct strbuf answer = STRBUF_INIT; int ret; ret = fsmonitor_ipc__send_command("quit", &answer); /* The quit command does not return any response data. */ strbuf_release(&answer); if (ret) return ret; trace2_region_enter("fsm_client", "polling-for-daemon-exit", NULL); while (fsmonitor_ipc__get_state() == IPC_STATE__LISTENING) sleep_millisec(50); trace2_region_leave("fsm_client", "polling-for-daemon-exit", NULL); return 0; } static int do_as_client__status(void) { enum ipc_active_state state = fsmonitor_ipc__get_state(); switch (state) { case IPC_STATE__LISTENING: printf(_("fsmonitor-daemon is watching '%s'\n"), the_repository->worktree); return 0; default: printf(_("fsmonitor-daemon is not watching '%s'\n"), the_repository->worktree); return 1; } } static ipc_server_application_cb handle_client; static int handle_client(void *data, const char *command, size_t command_len, ipc_server_reply_cb *reply, struct ipc_server_reply_data *reply_data) { /* struct fsmonitor_daemon_state *state = data; */ int result; /* * The Simple IPC API now supports {char*, len} arguments, but * FSMonitor always uses proper null-terminated strings, so * we can ignore the command_len argument. (Trust, but verify.) */ if (command_len != strlen(command)) BUG("FSMonitor assumes text messages"); trace2_region_enter("fsmonitor", "handle_client", the_repository); trace2_data_string("fsmonitor", the_repository, "request", command); result = 0; /* TODO Do something here. */ trace2_region_leave("fsmonitor", "handle_client", the_repository); return result; } static void *fsm_listen__thread_proc(void *_state) { struct fsmonitor_daemon_state *state = _state; trace2_thread_start("fsm-listen"); trace_printf_key(&trace_fsmonitor, "Watching: worktree '%s'", state->path_worktree_watch.buf); if (state->nr_paths_watching > 1) trace_printf_key(&trace_fsmonitor, "Watching: gitdir '%s'", state->path_gitdir_watch.buf); fsm_listen__loop(state); trace2_thread_exit(); return NULL; } static int fsmonitor_run_daemon_1(struct fsmonitor_daemon_state *state) { struct ipc_server_opts ipc_opts = { .nr_threads = fsmonitor__ipc_threads, /* * We know that there are no other active threads yet, * so we can let the IPC layer temporarily chdir() if * it needs to when creating the server side of the * Unix domain socket. */ .uds_disallow_chdir = 0 }; /* * Start the IPC thread pool before the we've started the file * system event listener thread so that we have the IPC handle * before we need it. */ if (ipc_server_run_async(&state->ipc_server_data, fsmonitor_ipc__get_path(), &ipc_opts, handle_client, state)) return error_errno( _("could not start IPC thread pool on '%s'"), fsmonitor_ipc__get_path()); /* * Start the fsmonitor listener thread to collect filesystem * events. */ if (pthread_create(&state->listener_thread, NULL, fsm_listen__thread_proc, state) < 0) { ipc_server_stop_async(state->ipc_server_data); ipc_server_await(state->ipc_server_data); return error(_("could not start fsmonitor listener thread")); } /* * The daemon is now fully functional in background threads. * Wait for the IPC thread pool to shutdown (whether by client * request or from filesystem activity). */ ipc_server_await(state->ipc_server_data); /* * The fsmonitor listener thread may have received a shutdown * event from the IPC thread pool, but it doesn't hurt to tell * it again. And wait for it to shutdown. */ fsm_listen__stop_async(state); pthread_join(state->listener_thread, NULL); return state->error_code; } static int fsmonitor_run_daemon(void) { struct fsmonitor_daemon_state state; int err; memset(&state, 0, sizeof(state)); pthread_mutex_init(&state.main_lock, NULL); state.error_code = 0; state.current_token_data = NULL; /* Prepare to (recursively) watch the directory. */ strbuf_init(&state.path_worktree_watch, 0); strbuf_addstr(&state.path_worktree_watch, absolute_path(get_git_work_tree())); state.nr_paths_watching = 1; /* * We create and delete cookie files somewhere inside the .git * directory to help us keep sync with the file system. If * ".git" is not a directory, then is not inside the * cone of , so set up a second watch to watch * the so that we get events for the cookie files. */ strbuf_init(&state.path_gitdir_watch, 0); strbuf_addbuf(&state.path_gitdir_watch, &state.path_worktree_watch); strbuf_addstr(&state.path_gitdir_watch, "/.git"); if (!is_directory(state.path_gitdir_watch.buf)) { strbuf_reset(&state.path_gitdir_watch); strbuf_addstr(&state.path_gitdir_watch, absolute_path(get_git_dir())); state.nr_paths_watching = 2; } /* * Confirm that we can create platform-specific resources for the * filesystem listener before we bother starting all the threads. */ if (fsm_listen__ctor(&state)) { err = error(_("could not initialize listener thread")); goto done; } err = fsmonitor_run_daemon_1(&state); done: pthread_mutex_destroy(&state.main_lock); fsm_listen__dtor(&state); ipc_server_free(state.ipc_server_data); strbuf_release(&state.path_worktree_watch); strbuf_release(&state.path_gitdir_watch); return err; } static int try_to_run_foreground_daemon(int detach_console) { /* * Technically, we don't need to probe for an existing daemon * process, since we could just call `fsmonitor_run_daemon()` * and let it fail if the pipe/socket is busy. * * However, this method gives us a nicer error message for a * common error case. */ if (fsmonitor_ipc__get_state() == IPC_STATE__LISTENING) die(_("fsmonitor--daemon is already running '%s'"), the_repository->worktree); if (fsmonitor__announce_startup) { fprintf(stderr, _("running fsmonitor-daemon in '%s'\n"), the_repository->worktree); fflush(stderr); } #ifdef GIT_WINDOWS_NATIVE if (detach_console) FreeConsole(); #endif return !!fsmonitor_run_daemon(); } static start_bg_wait_cb bg_wait_cb; static int bg_wait_cb(const struct child_process *cp, void *cb_data) { enum ipc_active_state s = fsmonitor_ipc__get_state(); switch (s) { case IPC_STATE__LISTENING: /* child is "ready" */ return 0; case IPC_STATE__NOT_LISTENING: case IPC_STATE__PATH_NOT_FOUND: /* give child more time */ return 1; default: case IPC_STATE__INVALID_PATH: case IPC_STATE__OTHER_ERROR: /* all the time in world won't help */ return -1; } } static int try_to_start_background_daemon(void) { struct child_process cp = CHILD_PROCESS_INIT; enum start_bg_result sbgr; /* * Before we try to create a background daemon process, see * if a daemon process is already listening. This makes it * easier for us to report an already-listening error to the * console, since our spawn/daemon can only report the success * of creating the background process (and not whether it * immediately exited). */ if (fsmonitor_ipc__get_state() == IPC_STATE__LISTENING) die(_("fsmonitor--daemon is already running '%s'"), the_repository->worktree); if (fsmonitor__announce_startup) { fprintf(stderr, _("starting fsmonitor-daemon in '%s'\n"), the_repository->worktree); fflush(stderr); } cp.git_cmd = 1; strvec_push(&cp.args, "fsmonitor--daemon"); strvec_push(&cp.args, "run"); strvec_push(&cp.args, "--detach"); strvec_pushf(&cp.args, "--ipc-threads=%d", fsmonitor__ipc_threads); cp.no_stdin = 1; cp.no_stdout = 1; cp.no_stderr = 1; sbgr = start_bg_command(&cp, bg_wait_cb, NULL, fsmonitor__start_timeout_sec); switch (sbgr) { case SBGR_READY: return 0; default: case SBGR_ERROR: case SBGR_CB_ERROR: return error(_("daemon failed to start")); case SBGR_TIMEOUT: return error(_("daemon not online yet")); case SBGR_DIED: return error(_("daemon terminated")); } } int cmd_fsmonitor__daemon(int argc, const char **argv, const char *prefix) { const char *subcmd; int detach_console = 0; struct option options[] = { OPT_BOOL(0, "detach", &detach_console, N_("detach from console")), OPT_INTEGER(0, "ipc-threads", &fsmonitor__ipc_threads, N_("use ipc worker threads")), OPT_INTEGER(0, "start-timeout", &fsmonitor__start_timeout_sec, N_("max seconds to wait for background daemon startup")), OPT_END() }; git_config(fsmonitor_config, NULL); argc = parse_options(argc, argv, prefix, options, builtin_fsmonitor__daemon_usage, 0); if (argc != 1) usage_with_options(builtin_fsmonitor__daemon_usage, options); subcmd = argv[0]; if (fsmonitor__ipc_threads < 1) die(_("invalid 'ipc-threads' value (%d)"), fsmonitor__ipc_threads); if (!strcmp(subcmd, "start")) return !!try_to_start_background_daemon(); if (!strcmp(subcmd, "run")) return !!try_to_run_foreground_daemon(detach_console); if (!strcmp(subcmd, "stop")) return !!do_as_client__send_stop(); if (!strcmp(subcmd, "status")) return !!do_as_client__status(); die(_("Unhandled subcommand '%s'"), subcmd); } #else int cmd_fsmonitor__daemon(int argc, const char **argv, const char *prefix) { struct option options[] = { OPT_END() }; if (argc == 2 && !strcmp(argv[1], "-h")) usage_with_options(builtin_fsmonitor__daemon_usage, options); die(_("fsmonitor--daemon not supported on this platform")); } #endif