Skip to content

Commit 3797a0a

Browse files
derrickstoleesunshineco
authored andcommitted
maintenance: use Windows scheduled tasks
Git's background maintenance uses cron by default, but this is not available on Windows. Instead, integrate with Task Scheduler. Tasks can be scheduled using the 'schtasks' command. There are several command-line options that can allow for some advanced scheduling, but unfortunately these seem to all require authenticating using a password. Instead, use the "/xml" option to pass an XML file that contains the configuration for the necessary schedule. These XML files are based on some that I exported after constructing a schedule in the Task Scheduler GUI. These options only run background maintenance when the user is logged in, and more fields are populated with the current username and SID at run-time by 'schtasks'. Since the GIT_TEST_MAINT_SCHEDULER environment variable allows us to specify 'schtasks' as the scheduler, we can test the Windows-specific logic on other platforms. Thus, add a check that the XML file written by Git is valid when xmllint exists on the system. Since we use a temporary file for the XML files sent to 'schtasks', we prefix the random characters with the frequency so it is easier to examine the proper file during tests. Instead of an exact match on the 'args' file, we 'grep' for the arguments other than the filename. There is a deficiency in the current design. Windows has two kinds of applications: GUI applications that start by "winmain()" and console applications that start by "main()". Console applications are attached to a new Console window if they are not already associated with a GUI application. This means that every hour the scheudled task launches a command window for the scheduled tasks. Not only is this visually obtrusive, but it also takes focus from whatever else the user is doing! A simple fix would be to insert a GUI application that acts as a shim between the scheduled task and Git. This is currently possible in Git for Windows by setting the <Command> tag equal to C:\Program Files\Git\git-bash.exe with options "--hide --no-needs-console --command=cmd\git.exe" followed by the arguments currently used. Since git-bash.exe is not included in Windows builds of core Git, I chose to leave out this feature. My plan is to submit a small patch to Git for Windows that converts the use of git.exe with this use of git-bash.exe in the short term. In the long term, we can consider creating this GUI shim application within core Git, perhaps in contrib/. Co-authored-by: Eric Sunshine <[email protected]> Signed-off-by: Eric Sunshine <[email protected]> Signed-off-by: Derrick Stolee <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent 2afe7e3 commit 3797a0a

File tree

3 files changed

+226
-1
lines changed

3 files changed

+226
-1
lines changed

Documentation/git-maintenance.txt

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,28 @@ To create more advanced customizations to your background tasks, see
313313
launchctl.plist(5) for more information.
314314

315315

316+
BACKGROUND MAINTENANCE ON WINDOWS SYSTEMS
317+
-----------------------------------------
318+
319+
Windows does not support `cron` and instead has its own system for
320+
scheduling background tasks. The `git maintenance start` command uses
321+
the `schtasks` command to submit tasks to this system. You can inspect
322+
all background tasks using the Task Scheduler application. The tasks
323+
added by Git have names of the form `Git Maintenance (<frequency>)`.
324+
The Task Scheduler GUI has ways to inspect these tasks, but you can also
325+
export the tasks to XML files and view the details there.
326+
327+
Note that since Git is a console application, these background tasks
328+
create a console window visible to the current user. This can be changed
329+
manually by selecting the "Run whether user is logged in or not" option
330+
in Task Scheduler. This change requires a password input, which is why
331+
`git maintenance start` does not select it by default.
332+
333+
If you want to customize the background tasks, please rename the tasks
334+
so future calls to `git maintenance (start|stop)` do not overwrite your
335+
custom tasks.
336+
337+
316338
GIT
317339
---
318340
Part of the linkgit:git[1] suite

builtin/gc.c

Lines changed: 167 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1589,7 +1589,7 @@ static int launchctl_schedule_plist(const char *exec_path, enum schedule_priorit
15891589
die(_("failed to create directories for '%s'"), filename);
15901590
plist = xfopen(filename, "w");
15911591

1592-
preamble = "<?xml version=\"1.0\" encoding=\"US-ASCII\"?>\n"
1592+
preamble = "<?xml version=\"1.0\"?>\n"
15931593
"<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n"
15941594
"<plist version=\"1.0\">"
15951595
"<dict>\n"
@@ -1671,6 +1671,168 @@ static int launchctl_update_schedule(int run_maintenance, int fd, const char *cm
16711671
return launchctl_remove_plists(cmd);
16721672
}
16731673

1674+
static char *schtasks_task_name(const char *frequency)
1675+
{
1676+
struct strbuf label = STRBUF_INIT;
1677+
strbuf_addf(&label, "Git Maintenance (%s)", frequency);
1678+
return strbuf_detach(&label, NULL);
1679+
}
1680+
1681+
static int schtasks_remove_task(enum schedule_priority schedule, const char *cmd)
1682+
{
1683+
int result;
1684+
struct strvec args = STRVEC_INIT;
1685+
const char *frequency = get_frequency(schedule);
1686+
char *name = schtasks_task_name(frequency);
1687+
1688+
strvec_split(&args, cmd);
1689+
strvec_pushl(&args, "/delete", "/tn", name, "/f", NULL);
1690+
1691+
result = run_command_v_opt(args.v, 0);
1692+
1693+
strvec_clear(&args);
1694+
free(name);
1695+
return result;
1696+
}
1697+
1698+
static int schtasks_remove_tasks(const char *cmd)
1699+
{
1700+
return schtasks_remove_task(SCHEDULE_HOURLY, cmd) ||
1701+
schtasks_remove_task(SCHEDULE_DAILY, cmd) ||
1702+
schtasks_remove_task(SCHEDULE_WEEKLY, cmd);
1703+
}
1704+
1705+
static int schtasks_schedule_task(const char *exec_path, enum schedule_priority schedule, const char *cmd)
1706+
{
1707+
int result;
1708+
struct child_process child = CHILD_PROCESS_INIT;
1709+
const char *xml;
1710+
struct tempfile *tfile;
1711+
const char *frequency = get_frequency(schedule);
1712+
char *name = schtasks_task_name(frequency);
1713+
struct strbuf tfilename = STRBUF_INIT;
1714+
1715+
strbuf_addf(&tfilename, "%s/schedule_%s_XXXXXX",
1716+
get_git_common_dir(), frequency);
1717+
tfile = xmks_tempfile(tfilename.buf);
1718+
strbuf_release(&tfilename);
1719+
1720+
if (!fdopen_tempfile(tfile, "w"))
1721+
die(_("failed to create temp xml file"));
1722+
1723+
xml = "<?xml version=\"1.0\" ?>\n"
1724+
"<Task version=\"1.4\" xmlns=\"http://schemas.microsoft.com/windows/2004/02/mit/task\">\n"
1725+
"<Triggers>\n"
1726+
"<CalendarTrigger>\n";
1727+
fputs(xml, tfile->fp);
1728+
1729+
switch (schedule) {
1730+
case SCHEDULE_HOURLY:
1731+
fprintf(tfile->fp,
1732+
"<StartBoundary>2020-01-01T01:00:00</StartBoundary>\n"
1733+
"<Enabled>true</Enabled>\n"
1734+
"<ScheduleByDay>\n"
1735+
"<DaysInterval>1</DaysInterval>\n"
1736+
"</ScheduleByDay>\n"
1737+
"<Repetition>\n"
1738+
"<Interval>PT1H</Interval>\n"
1739+
"<Duration>PT23H</Duration>\n"
1740+
"<StopAtDurationEnd>false</StopAtDurationEnd>\n"
1741+
"</Repetition>\n");
1742+
break;
1743+
1744+
case SCHEDULE_DAILY:
1745+
fprintf(tfile->fp,
1746+
"<StartBoundary>2020-01-01T00:00:00</StartBoundary>\n"
1747+
"<Enabled>true</Enabled>\n"
1748+
"<ScheduleByWeek>\n"
1749+
"<DaysOfWeek>\n"
1750+
"<Monday />\n"
1751+
"<Tuesday />\n"
1752+
"<Wednesday />\n"
1753+
"<Thursday />\n"
1754+
"<Friday />\n"
1755+
"<Saturday />\n"
1756+
"</DaysOfWeek>\n"
1757+
"<WeeksInterval>1</WeeksInterval>\n"
1758+
"</ScheduleByWeek>\n");
1759+
break;
1760+
1761+
case SCHEDULE_WEEKLY:
1762+
fprintf(tfile->fp,
1763+
"<StartBoundary>2020-01-01T00:00:00</StartBoundary>\n"
1764+
"<Enabled>true</Enabled>\n"
1765+
"<ScheduleByWeek>\n"
1766+
"<DaysOfWeek>\n"
1767+
"<Sunday />\n"
1768+
"</DaysOfWeek>\n"
1769+
"<WeeksInterval>1</WeeksInterval>\n"
1770+
"</ScheduleByWeek>\n");
1771+
break;
1772+
1773+
default:
1774+
break;
1775+
}
1776+
1777+
xml = "</CalendarTrigger>\n"
1778+
"</Triggers>\n"
1779+
"<Principals>\n"
1780+
"<Principal id=\"Author\">\n"
1781+
"<LogonType>InteractiveToken</LogonType>\n"
1782+
"<RunLevel>LeastPrivilege</RunLevel>\n"
1783+
"</Principal>\n"
1784+
"</Principals>\n"
1785+
"<Settings>\n"
1786+
"<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>\n"
1787+
"<Enabled>true</Enabled>\n"
1788+
"<Hidden>true</Hidden>\n"
1789+
"<UseUnifiedSchedulingEngine>true</UseUnifiedSchedulingEngine>\n"
1790+
"<WakeToRun>false</WakeToRun>\n"
1791+
"<ExecutionTimeLimit>PT72H</ExecutionTimeLimit>\n"
1792+
"<Priority>7</Priority>\n"
1793+
"</Settings>\n"
1794+
"<Actions Context=\"Author\">\n"
1795+
"<Exec>\n"
1796+
"<Command>\"%s\\git.exe\"</Command>\n"
1797+
"<Arguments>--exec-path=\"%s\" for-each-repo --config=maintenance.repo maintenance run --schedule=%s</Arguments>\n"
1798+
"</Exec>\n"
1799+
"</Actions>\n"
1800+
"</Task>\n";
1801+
fprintf(tfile->fp, xml, exec_path, exec_path, frequency);
1802+
strvec_split(&child.args, cmd);
1803+
strvec_pushl(&child.args, "/create", "/tn", name, "/f", "/xml",
1804+
get_tempfile_path(tfile), NULL);
1805+
close_tempfile_gently(tfile);
1806+
1807+
child.no_stdout = 1;
1808+
child.no_stderr = 1;
1809+
1810+
if (start_command(&child))
1811+
die(_("failed to start schtasks"));
1812+
result = finish_command(&child);
1813+
1814+
delete_tempfile(&tfile);
1815+
free(name);
1816+
return result;
1817+
}
1818+
1819+
static int schtasks_schedule_tasks(const char *cmd)
1820+
{
1821+
const char *exec_path = git_exec_path();
1822+
1823+
return schtasks_schedule_task(exec_path, SCHEDULE_HOURLY, cmd) ||
1824+
schtasks_schedule_task(exec_path, SCHEDULE_DAILY, cmd) ||
1825+
schtasks_schedule_task(exec_path, SCHEDULE_WEEKLY, cmd);
1826+
}
1827+
1828+
static int schtasks_update_schedule(int run_maintenance, int fd, const char *cmd)
1829+
{
1830+
if (run_maintenance)
1831+
return schtasks_schedule_tasks(cmd);
1832+
else
1833+
return schtasks_remove_tasks(cmd);
1834+
}
1835+
16741836
#define BEGIN_LINE "# BEGIN GIT MAINTENANCE SCHEDULE"
16751837
#define END_LINE "# END GIT MAINTENANCE SCHEDULE"
16761838

@@ -1761,6 +1923,8 @@ static int crontab_update_schedule(int run_maintenance, int fd, const char *cmd)
17611923

17621924
#if defined(__APPLE__)
17631925
static const char platform_scheduler[] = "launchctl";
1926+
#elif defined(GIT_WINDOWS_NATIVE)
1927+
static const char platform_scheduler[] = "schtasks";
17641928
#else
17651929
static const char platform_scheduler[] = "crontab";
17661930
#endif
@@ -1789,6 +1953,8 @@ static int update_background_schedule(int enable)
17891953

17901954
if (!strcmp(scheduler, "launchctl"))
17911955
result = launchctl_update_schedule(enable, lk.tempfile->fd, cmd);
1956+
else if (!strcmp(scheduler, "schtasks"))
1957+
result = schtasks_update_schedule(enable, lk.tempfile->fd, cmd);
17921958
else if (!strcmp(scheduler, "crontab"))
17931959
result = crontab_update_schedule(enable, lk.tempfile->fd, cmd);
17941960
else

t/t7900-maintenance.sh

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,43 @@ test_expect_success 'start and stop macOS maintenance' '
454454
test_line_count = 0 actual
455455
'
456456

457+
test_expect_success 'start and stop Windows maintenance' '
458+
write_script print-args <<-\EOF &&
459+
echo $* >>args
460+
while test $# -gt 0
461+
do
462+
case "$1" in
463+
/xml) shift; xmlfile=$1; break ;;
464+
*) shift ;;
465+
esac
466+
done
467+
test -z "$xmlfile" || cp "$xmlfile" "$xmlfile.xml"
468+
EOF
469+
470+
rm -f args &&
471+
GIT_TEST_MAINT_SCHEDULER="schtasks:./print-args" git maintenance start &&
472+
473+
# start registers the repo
474+
git config --get --global maintenance.repo "$(pwd)" &&
475+
476+
for frequency in hourly daily weekly
477+
do
478+
grep "/create /tn Git Maintenance ($frequency) /f /xml" args &&
479+
file=$(ls .git/schedule_${frequency}*.xml) &&
480+
test_xmllint "$file" || return 1
481+
done &&
482+
483+
rm -f args &&
484+
GIT_TEST_MAINT_SCHEDULER="schtasks:./print-args" git maintenance stop &&
485+
486+
# stop does not unregister the repo
487+
git config --get --global maintenance.repo "$(pwd)" &&
488+
489+
printf "/delete /tn Git Maintenance (%s) /f\n" \
490+
hourly daily weekly >expect &&
491+
test_cmp expect args
492+
'
493+
457494
test_expect_success 'register preserves existing strategy' '
458495
git config maintenance.strategy none &&
459496
git maintenance register &&

0 commit comments

Comments
 (0)