maintenance: skip bootout/bootstrap when plist is registered

On macOS, we use launchctl to manage the background maintenance schedule. This uses a set of .plist files to describe the schedule, but these files are also registered with 'launchctl bootstrap'. If multiple 'git maintenance start' commands run concurrently, then they can collide replacing these schedule files and registering them with launchctl. To avoid extra launchctl commands, do a check for the .plist files on disk and check if they are registered using 'launchctl list <name>'. This command will return with exit code 0 if it exists, or exit code 113 if it does not. We can test this behavior using the GIT_TEST_MAINT_SCHEDULER environment variable. Signed-off-by: Derrick Stolee <dstolee@microsoft.com> Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Derrick Stolee committed Aug 24, 2021 at 15:44 UTC a16eb6b1ff333f3e26b99f17ef3bb7dbf8135f39
2 files changed +62 -9
builtin/gc.c
+45 -9
@@ -1600,6 +1600,29 @@ static int launchctl_remove_plists(const char *cmd)
1600 launchctl_remove_plist(SCHEDULE_WEEKLY, cmd);
1601 }
1602
1603 +static int launchctl_list_contains_plist(const char *name, const char *cmd)
1604 +{
1605 + int result;
1606 + struct child_process child = CHILD_PROCESS_INIT;
1607 + char *uid = launchctl_get_uid();
1608 +
1609 + strvec_split(&child.args, cmd);
1610 + strvec_pushl(&child.args, "list", name, NULL);
1611 +
1612 + child.no_stderr = 1;
1613 + child.no_stdout = 1;
1614 +
1615 + if (start_command(&child))
1616 + die(_("failed to start launchctl"));
1617 +
1618 + result = finish_command(&child);
1619 +
1620 + free(uid);
1621 +
1622 + /* Returns failure if 'name' doesn't exist. */
1623 + return !result;
1624 +}
1625 +
1626 static int launchctl_schedule_plist(const char *exec_path, enum schedule_priority schedule, const char *cmd)
1627 {
1628 int i, fd;
@@ -1609,7 +1632,8 @@ static int launchctl_schedule_plist(const char *exec_path, enum schedule_priorit
1632 char *filename = launchctl_service_filename(name);
1633 struct lock_file lk = LOCK_INIT;
1634 static unsigned long lock_file_timeout_ms = ULONG_MAX;
1612 - struct strbuf plist = STRBUF_INIT;
1635 + struct strbuf plist = STRBUF_INIT, plist2 = STRBUF_INIT;
1636 + struct stat st;
1637
1638 preamble = "<?xml version=\"1.0\"?>\n"
1639 "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n"
@@ -1676,18 +1700,30 @@ static int launchctl_schedule_plist(const char *exec_path, enum schedule_priorit
1700 fd = hold_lock_file_for_update_timeout(&lk, filename, LOCK_DIE_ON_ERROR,
1701 lock_file_timeout_ms);
1702
1679 - if (write_in_full(fd, plist.buf, plist.len) < 0 ||
1680 - commit_lock_file(&lk))
1681 - die_errno(_("could not write '%s'"), filename);
1682 -
1683 - /* bootout might fail if not already running, so ignore */
1684 - launchctl_boot_plist(0, filename, cmd);
1685 - if (launchctl_boot_plist(1, filename, cmd))
1686 - die(_("failed to bootstrap service %s"), filename);
1703 + /*
1704 + * Does this file already exist? With the intended contents? Is it
1705 + * registered already? Then it does not need to be re-registered.
1706 + */
1707 + if (!stat(filename, &st) && st.st_size == plist.len &&
1708 + strbuf_read_file(&plist2, filename, plist.len) == plist.len &&
1709 + !strbuf_cmp(&plist, &plist2) &&
1710 + launchctl_list_contains_plist(name, cmd))
1711 + rollback_lock_file(&lk);
1712 + else {
1713 + if (write_in_full(fd, plist.buf, plist.len) < 0 ||
1714 + commit_lock_file(&lk))
1715 + die_errno(_("could not write '%s'"), filename);
1716 +
1717 + /* bootout might fail if not already running, so ignore */
1718 + launchctl_boot_plist(0, filename, cmd);
1719 + if (launchctl_boot_plist(1, filename, cmd))
1720 + die(_("failed to bootstrap service %s"), filename);
1721 + }
1722
1723 free(filename);
1724 free(name);
1725 strbuf_release(&plist);
1726 + strbuf_release(&plist2);
1727 return 0;
1728 }
1729
t/t7900-maintenance.sh
+17
@@ -582,6 +582,23 @@ test_expect_success 'start and stop macOS maintenance' '
582 test_line_count = 0 actual
583 '
584
585 +test_expect_success 'use launchctl list to prevent extra work' '
586 + # ensure we are registered
587 + GIT_TEST_MAINT_SCHEDULER=launchctl:./print-args git maintenance start &&
588 +
589 + # do it again on a fresh args file
590 + rm -f args &&
591 + GIT_TEST_MAINT_SCHEDULER=launchctl:./print-args git maintenance start &&
592 +
593 + ls "$HOME/Library/LaunchAgents" >actual &&
594 + cat >expect <<-\EOF &&
595 + list org.git-scm.git.hourly
596 + list org.git-scm.git.daily
597 + list org.git-scm.git.weekly
598 + EOF
599 + test_cmp expect args
600 +'
601 +
602 test_expect_success 'start and stop Windows maintenance' '
603 write_script print-args <<-\EOF &&
604 echo $* >>args