Skip to content

Commit bb438af

Browse files
committed
fsmonitor: close inherited file descriptors and detach in daemon
When the fsmonitor daemon is spawned as a background process, it may inherit file descriptors from its parent that it does not need. In particular, when the test harness or a CI system captures output through pipes, the daemon can inherit duplicated pipe endpoints. If the daemon holds these open, the parent process never sees EOF and may appear to hang. Set close_fd_above_stderr on the child process at daemon startup so that file descriptors 3 and above are closed before any daemon work begins. This ensures the daemon does not inadvertently hold open descriptors from its launching environment. Additionally, call setsid() when the daemon starts with --detach to create a new session and process group. Without this, shells that enable job control (e.g. bash with "set -m") treat the daemon as part of the spawning command's job. Their "wait" builtin then blocks until the daemon exits, which it never does. This specifically affects systems where /bin/sh is bash (e.g. Fedora), since dash only waits for the specific PID rather than the full process group. Add a 30-second timeout to "fsmonitor--daemon stop" so it does not block indefinitely if the daemon fails to shut down. Signed-off-by: Paul Tarjan <github@paulisageek.com>
1 parent 817489b commit bb438af

4 files changed

Lines changed: 40 additions & 11 deletions

File tree

builtin/fsmonitor--daemon.c

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ static int do_as_client__send_stop(void)
8686
{
8787
struct strbuf answer = STRBUF_INIT;
8888
int ret;
89+
int max_wait_ms = 30000;
90+
int elapsed_ms = 0;
8991

9092
ret = fsmonitor_ipc__send_command("quit", &answer);
9193

@@ -96,8 +98,16 @@ static int do_as_client__send_stop(void)
9698
return ret;
9799

98100
trace2_region_enter("fsm_client", "polling-for-daemon-exit", NULL);
99-
while (fsmonitor_ipc__get_state() == IPC_STATE__LISTENING)
101+
while (fsmonitor_ipc__get_state() == IPC_STATE__LISTENING) {
102+
if (elapsed_ms >= max_wait_ms) {
103+
trace2_region_leave("fsm_client",
104+
"polling-for-daemon-exit", NULL);
105+
return error(_("daemon did not stop within %d seconds"),
106+
max_wait_ms / 1000);
107+
}
100108
sleep_millisec(50);
109+
elapsed_ms += 50;
110+
}
101111
trace2_region_leave("fsm_client", "polling-for-daemon-exit", NULL);
102112

103113
return 0;
@@ -1431,7 +1441,7 @@ static int fsmonitor_run_daemon(void)
14311441
return err;
14321442
}
14331443

1434-
static int try_to_run_foreground_daemon(int detach_console MAYBE_UNUSED)
1444+
static int try_to_run_foreground_daemon(int detach_console)
14351445
{
14361446
/*
14371447
* Technically, we don't need to probe for an existing daemon
@@ -1451,10 +1461,21 @@ static int try_to_run_foreground_daemon(int detach_console MAYBE_UNUSED)
14511461
fflush(stderr);
14521462
}
14531463

1464+
if (detach_console) {
14541465
#ifdef GIT_WINDOWS_NATIVE
1455-
if (detach_console)
14561466
FreeConsole();
1467+
#else
1468+
/*
1469+
* Create a new session so that the daemon is detached
1470+
* from the parent's process group. This prevents
1471+
* shells with job control (e.g. bash with "set -m")
1472+
* from waiting on the daemon when they wait for a
1473+
* foreground command that implicitly spawned it.
1474+
*/
1475+
if (setsid() == -1)
1476+
warning_errno(_("setsid failed"));
14571477
#endif
1478+
}
14581479

14591480
return !!fsmonitor_run_daemon();
14601481
}
@@ -1517,6 +1538,7 @@ static int try_to_start_background_daemon(void)
15171538
cp.no_stdin = 1;
15181539
cp.no_stdout = 1;
15191540
cp.no_stderr = 1;
1541+
cp.close_fd_above_stderr = 1;
15201542

15211543
sbgr = start_bg_command(&cp, bg_wait_cb, NULL,
15221544
fsmonitor__start_timeout_sec);

fsmonitor-ipc.c

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ static int spawn_daemon(void)
6161

6262
cmd.git_cmd = 1;
6363
cmd.no_stdin = 1;
64+
cmd.no_stdout = 1;
65+
cmd.no_stderr = 1;
66+
cmd.close_fd_above_stderr = 1;
6467
cmd.trace2_child_class = "fsmonitor";
6568
strvec_pushl(&cmd.args, "fsmonitor--daemon", "start", NULL);
6669

t/meson.build

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1210,18 +1210,12 @@ test_environment = script_environment
12101210
test_environment.set('GIT_BUILD_DIR', git_build_dir)
12111211

12121212
foreach integration_test : integration_tests
1213-
per_test_kwargs = test_kwargs
1214-
# The fsmonitor tests start daemon processes that in some environments
1215-
# can hang. Set a generous timeout to prevent CI from blocking.
1216-
if fs.stem(integration_test) == 't7527-builtin-fsmonitor'
1217-
per_test_kwargs += {'timeout': 1800}
1218-
endif
12191213
test(fs.stem(integration_test), shell,
12201214
args: [ integration_test ],
12211215
workdir: meson.current_source_dir(),
12221216
env: test_environment,
12231217
depends: test_dependencies + bin_wrappers,
1224-
kwargs: per_test_kwargs,
1218+
kwargs: test_kwargs,
12251219
)
12261220
endforeach
12271221

t/t7527-builtin-fsmonitor.sh

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -766,7 +766,7 @@ do
766766
else
767767
test_expect_success "Matrix[uc:$uc_val][fsm:$fsm_val] enable fsmonitor" '
768768
git config core.fsmonitor true &&
769-
git fsmonitor--daemon start &&
769+
git fsmonitor--daemon start --start-timeout=10 &&
770770
git update-index --fsmonitor
771771
'
772772
fi
@@ -997,7 +997,17 @@ start_git_in_background () {
997997
nr_tries_left=$(($nr_tries_left - 1))
998998
done >/dev/null 2>&1 3>&- 4>&- 5>&- 6>&- 7>&- &
999999
watchdog_pid=$!
1000+
1001+
# Disable job control before wait. With "set -m", bash treats
1002+
# "wait $pid" as waiting for the entire job (process group),
1003+
# which blocks indefinitely if the fsmonitor daemon was spawned
1004+
# into the same process group and is still running. Turning off
1005+
# job control makes "wait" only wait for the specific PID.
1006+
set +m &&
10001007
wait $git_pid
1008+
wait_status=$?
1009+
set -m
1010+
return $wait_status
10011011
}
10021012

10031013
stop_git () {

0 commit comments

Comments
 (0)