Skip to content

Commit 6e95b07

Browse files
pks-tttaylorr
authored andcommitted
builtin/maintenance: fix locking with "--detach"
When running git-maintenance(1), we create a lockfile that is supposed to keep other maintenance processes from running at the same time. This lockfile is broken though in case the "--detach" flag is passed: the lockfile is created by the parent process and will be cleaned up either manually or on exit. But when detaching, the parent will exit before all of the background maintenance tasks have been run, and consequently the lock only covers a smaller part of the whole maintenance process. Fix this bug by reassigning all tempfiles from the parent process to the child process when daemonizing so that it becomes the responsibility of the child to clean them up. Note that this is a broader fix, as we now always reassign tempfiles when daemonizing. This is a natural consequence of the semantics of `daemonize()` though, as it essentially promises to continue running the current process in the background. It is thus sensible to have that function perform the whole dance of assigning resources to the child process, including tempfiles. There's only a single other caller in "daemon.c", but that process doesn't create any tempfiles before the call to `daemonize()` and is thus not impacted by this change. Reported-by: Jean-Christophe Manciot <actionmystique@gmail.com> Helped-by: Jeff King <peff@peff.net> Helped-by: Derrick Stolee <stolee@gmail.com> Co-authored-by: Taylor Blau <me@ttaylorr.com> Signed-off-by: Taylor Blau <me@ttaylorr.com> Signed-off-by: Patrick Steinhardt <ps@pks.im> Signed-off-by: Junio C Hamano <gitster@pobox.com>
1 parent 94f0577 commit 6e95b07

5 files changed

Lines changed: 111 additions & 1 deletion

File tree

setup.c

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2162,12 +2162,26 @@ int daemonize(void)
21622162
errno = ENOSYS;
21632163
return -1;
21642164
#else
2165-
switch (fork()) {
2165+
pid_t parent_pid = getpid();
2166+
pid_t child_pid = fork();
2167+
2168+
switch (child_pid) {
21662169
case 0:
2170+
/*
2171+
* We're in the child process, so we take ownership of
2172+
* all tempfiles.
2173+
*/
2174+
reassign_tempfile_ownership(parent_pid, getpid());
21672175
break;
21682176
case -1:
21692177
die_errno(_("fork failed"));
21702178
default:
2179+
/*
2180+
* We're in the parent process, so we drop ownership of
2181+
* all tempfiles to prevent us from removing them upon
2182+
* exit.
2183+
*/
2184+
reassign_tempfile_ownership(parent_pid, child_pid);
21712185
exit(0);
21722186
}
21732187
if (setsid() == -1)

setup.h

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,21 @@ void verify_non_filename(const char *prefix, const char *name);
149149
int path_inside_repo(const char *prefix, const char *path);
150150

151151
void sanitize_stdfds(void);
152+
153+
/*
154+
* Daemonize the current process by forking and then exiting the parent
155+
* process. Returns 0 when successful, in which case the parent process will
156+
* have exited and it's the child process that continues to run the code.
157+
* Otherwise, a negative error code is returned and the parent process will
158+
* continue execution.
159+
*
160+
* Note that this function will also perform the following changes:
161+
*
162+
* - Standard file descriptors in the child process are closed.
163+
* - The child process is made a session leader via setsid(3p).
164+
* - All tempfiles owned by the parent process are reassigned to the
165+
* daemonized child process.
166+
*/
152167
int daemonize(void);
153168

154169
/*

t/t7900-maintenance.sh

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1438,6 +1438,64 @@ test_expect_success '--no-detach causes maintenance to not run in background' '
14381438
)
14391439
'
14401440

1441+
test_expect_success PIPE '--detach holds maintenance lock until daemonized child exits' '
1442+
test_when_finished "rm -rf repo" &&
1443+
git init repo &&
1444+
(
1445+
cd repo &&
1446+
1447+
git config maintenance.auto false &&
1448+
git config core.lockfilepid true &&
1449+
1450+
git remote add origin /does/not/exist &&
1451+
git config set remote.origin.uploadpack "cat fifo-uploadpack" &&
1452+
1453+
mkfifo fifo-uploadpack fifo-maint &&
1454+
1455+
# Open the maintenance FIFO, as otherwise spawning
1456+
# git-maintenance(1) would block. Note that we need to open it
1457+
# as read-write, as otherwise we would block here already.
1458+
exec 9<>fifo-maint &&
1459+
1460+
{ git maintenance run --task=prefetch --detach 7>&9 & } &&
1461+
parent="$!" &&
1462+
1463+
# Reap the parent process so that the exec call below will not
1464+
# get SIGCHLD.
1465+
wait "$parent" &&
1466+
1467+
# Open the git-upload-pack(1) FIFO for writing, which will
1468+
# block until the upload-pack script opens it for reading. Once
1469+
# exec returns, we know that the daemonized child is alive and
1470+
# pinned.
1471+
exec 8>fifo-uploadpack &&
1472+
1473+
test_path_is_file .git/objects/maintenance.lock &&
1474+
test_path_is_file .git/objects/"maintenance~pid.lock" &&
1475+
1476+
# Verify that the maintenance.lock still exists, and
1477+
# that it was created by the parent process, not the
1478+
# child.
1479+
echo "pid $parent" >expect &&
1480+
test_cmp expect .git/objects/"maintenance~pid.lock" &&
1481+
1482+
# Reopen the maintenance FIFO as read-only so that
1483+
# git-maintenance(1) is the only writer. This will cause it to
1484+
# close the FIFO once the process exits.
1485+
exec 9<&- &&
1486+
exec 9<fifo-maint &&
1487+
1488+
# Close the FIFO used by git-upload-pack(1) to unblock it and
1489+
# then wait until the maintenance FIFO is closed by
1490+
# git-maintenance(1), indicating that it has exited.
1491+
exec 8>&- &&
1492+
cat <&9 &&
1493+
1494+
test_path_is_missing .git/objects/maintenance.lock &&
1495+
test_path_is_missing .git/objects/"maintenance~pid.lock"
1496+
)
1497+
'
1498+
14411499
test_expect_success '--detach causes maintenance to run in background' '
14421500
test_when_finished "rm -rf repo" &&
14431501
git init repo &&

tempfile.c

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,3 +373,15 @@ int delete_tempfile(struct tempfile **tempfile_p)
373373

374374
return err ? -1 : 0;
375375
}
376+
377+
void reassign_tempfile_ownership(pid_t from, pid_t to)
378+
{
379+
volatile struct volatile_list_head *pos;
380+
381+
list_for_each(pos, &tempfile_list) {
382+
struct tempfile *p = list_entry(pos, struct tempfile, list);
383+
384+
if (is_tempfile_active(p) && p->owner == from)
385+
p->owner = to;
386+
}
387+
}

tempfile.h

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,4 +282,15 @@ int delete_tempfile(struct tempfile **tempfile_p);
282282
*/
283283
int rename_tempfile(struct tempfile **tempfile_p, const char *path);
284284

285+
/*
286+
* Reassign ownership of all active tempfiles whose `owner` field matches
287+
* `from` to `to`.
288+
*
289+
* This is intended for use by `daemonize()`; after `fork(2)`-ing, the parent
290+
* transfers ownership to the daemonized child so that its atexit handler does
291+
* not unlink tempfiles that should outlive it, and the child claims the
292+
* inherited tempfiles so that they are cleaned up when the daemon exits.
293+
*/
294+
void reassign_tempfile_ownership(pid_t from, pid_t to);
295+
285296
#endif /* TEMPFILE_H */

0 commit comments

Comments
 (0)