Skip to content

Commit 6f92f94

Browse files
committed
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'. 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/. Signed-off-by: Derrick Stolee <[email protected]>
1 parent 49cecb9 commit 6f92f94

File tree

3 files changed

+236
-3
lines changed

3 files changed

+236
-3
lines changed

Documentation/git-maintenance.txt

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,28 @@ https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPSy
316316
for more information.
317317

318318

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

builtin/gc.c

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1698,6 +1698,187 @@ static int platform_update_schedule(int run_maintenance, int fd)
16981698
else
16991699
return remove_plists();
17001700
}
1701+
1702+
#elif defined(GIT_WINDOWS_NATIVE)
1703+
1704+
static const char *get_frequency(enum schedule_priority schedule)
1705+
{
1706+
switch (schedule) {
1707+
case SCHEDULE_HOURLY:
1708+
return "hourly";
1709+
case SCHEDULE_DAILY:
1710+
return "daily";
1711+
case SCHEDULE_WEEKLY:
1712+
return "weekly";
1713+
default:
1714+
BUG("invalid schedule %d", schedule);
1715+
}
1716+
}
1717+
1718+
static char *get_task_name(const char *frequency)
1719+
{
1720+
struct strbuf label = STRBUF_INIT;
1721+
strbuf_addf(&label, "Git Maintenance (%s)", frequency);
1722+
return strbuf_detach(&label, NULL);
1723+
}
1724+
1725+
static int remove_task(enum schedule_priority schedule)
1726+
{
1727+
int result;
1728+
struct strvec args = STRVEC_INIT;
1729+
const char *frequency = get_frequency(schedule);
1730+
char *name = get_task_name(frequency);
1731+
const char *schtasks = getenv("GIT_TEST_CRONTAB");
1732+
if (!schtasks)
1733+
schtasks = "schtasks";
1734+
1735+
strvec_split(&args, schtasks);
1736+
strvec_pushl(&args, "/delete", "/tn", name, "/f", NULL);
1737+
1738+
result = run_command_v_opt(args.v, 0);
1739+
1740+
strvec_clear(&args);
1741+
free(name);
1742+
return result;
1743+
}
1744+
1745+
static int remove_scheduled_tasks(void)
1746+
{
1747+
return remove_task(SCHEDULE_HOURLY) ||
1748+
remove_task(SCHEDULE_DAILY) ||
1749+
remove_task(SCHEDULE_WEEKLY);
1750+
}
1751+
1752+
static int schedule_task(const char *exec_path, enum schedule_priority schedule)
1753+
{
1754+
int result;
1755+
struct strvec args = STRVEC_INIT;
1756+
const char *xml, *schtasks;
1757+
char *xmlpath;
1758+
FILE *xmlfp;
1759+
const char *frequency = get_frequency(schedule);
1760+
char *name = get_task_name(frequency);
1761+
1762+
xmlpath = xstrfmt("%s/schedule-%s.xml",
1763+
the_repository->objects->odb->path,
1764+
frequency);
1765+
xmlfp = fopen(xmlpath, "w");
1766+
if (!xmlfp)
1767+
die(_("failed to open '%s'"), xmlpath);
1768+
1769+
xml = "<?xml version=\"1.0\" encoding=\"UTF-16\"?>\n"
1770+
"<Task version=\"1.4\" xmlns=\"http://schemas.microsoft.com/windows/2004/02/mit/task\">\n"
1771+
"<Triggers>\n"
1772+
"<CalendarTrigger>\n";
1773+
fprintf(xmlfp, xml);
1774+
1775+
switch (schedule) {
1776+
case SCHEDULE_HOURLY:
1777+
fprintf(xmlfp,
1778+
"<StartBoundary>2020-01-01T01:00:00</StartBoundary>\n"
1779+
"<Enabled>true</Enabled>\n"
1780+
"<ScheduleByDay>\n"
1781+
"<DaysInterval>1</DaysInterval>\n"
1782+
"</ScheduleByDay>\n"
1783+
"<Repetition>\n"
1784+
"<Interval>PT1H</Interval>\n"
1785+
"<Duration>PT23H</Duration>\n"
1786+
"<StopAtDurationEnd>false</StopAtDurationEnd>\n"
1787+
"</Repetition>\n");
1788+
break;
1789+
1790+
case SCHEDULE_DAILY:
1791+
fprintf(xmlfp,
1792+
"<StartBoundary>2020-01-01T00:00:00</StartBoundary>\n"
1793+
"<Enabled>true</Enabled>\n"
1794+
"<ScheduleByWeek>\n"
1795+
"<DaysOfWeek>\n"
1796+
"<Monday />\n"
1797+
"<Tuesday />\n"
1798+
"<Wednesday />\n"
1799+
"<Thursday />\n"
1800+
"<Friday />\n"
1801+
"<Saturday />\n"
1802+
"</DaysOfWeek>\n"
1803+
"<WeeksInterval>1</WeeksInterval>\n"
1804+
"</ScheduleByWeek>\n");
1805+
break;
1806+
1807+
case SCHEDULE_WEEKLY:
1808+
fprintf(xmlfp,
1809+
"<StartBoundary>2020-01-01T00:00:00</StartBoundary>\n"
1810+
"<Enabled>true</Enabled>\n"
1811+
"<ScheduleByWeek>\n"
1812+
"<DaysOfWeek>\n"
1813+
"<Sunday />\n"
1814+
"</DaysOfWeek>\n"
1815+
"<WeeksInterval>1</WeeksInterval>\n"
1816+
"</ScheduleByWeek>\n");
1817+
break;
1818+
1819+
default:
1820+
break;
1821+
}
1822+
1823+
xml= "</CalendarTrigger>\n"
1824+
"</Triggers>\n"
1825+
"<Principals>\n"
1826+
"<Principal id=\"Author\">\n"
1827+
"<LogonType>InteractiveToken</LogonType>\n"
1828+
"<RunLevel>LeastPrivilege</RunLevel>\n"
1829+
"</Principal>\n"
1830+
"</Principals>\n"
1831+
"<Settings>\n"
1832+
"<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>\n"
1833+
"<Enabled>true</Enabled>\n"
1834+
"<Hidden>true</Hidden>\n"
1835+
"<UseUnifiedSchedulingEngine>true</UseUnifiedSchedulingEngine>\n"
1836+
"<WakeToRun>false</WakeToRun>\n"
1837+
"<ExecutionTimeLimit>PT72H</ExecutionTimeLimit>\n"
1838+
"<Priority>7</Priority>\n"
1839+
"</Settings>\n"
1840+
"<Actions Context=\"Author\">\n"
1841+
"<Exec>\n"
1842+
"<Command>\"%s\\git.exe\"</Command>\n"
1843+
"<Arguments>--exec-path=\"%s\" for-each-repo --config=maintenance.repo maintenance run --schedule=%s</Arguments>\n"
1844+
"</Exec>\n"
1845+
"</Actions>\n"
1846+
"</Task>\n";
1847+
fprintf(xmlfp, xml, exec_path, exec_path, frequency);
1848+
fclose(xmlfp);
1849+
1850+
schtasks = getenv("GIT_TEST_CRONTAB");
1851+
if (!schtasks)
1852+
schtasks = "schtasks";
1853+
strvec_split(&args, schtasks);
1854+
strvec_pushl(&args, "/create", "/tn", name, "/f", "/xml", xmlpath, NULL);
1855+
1856+
result = run_command_v_opt(args.v, 0);
1857+
1858+
strvec_clear(&args);
1859+
unlink(xmlpath);
1860+
free(xmlpath);
1861+
free(name);
1862+
return result;
1863+
}
1864+
1865+
static int add_scheduled_tasks(void)
1866+
{
1867+
const char *exec_path = git_exec_path();
1868+
1869+
return schedule_task(exec_path, SCHEDULE_HOURLY) ||
1870+
schedule_task(exec_path, SCHEDULE_DAILY) ||
1871+
schedule_task(exec_path, SCHEDULE_WEEKLY);
1872+
}
1873+
1874+
static int platform_update_schedule(int run_maintenance, int fd)
1875+
{
1876+
if (run_maintenance)
1877+
return add_scheduled_tasks();
1878+
else
1879+
return remove_scheduled_tasks();
1880+
}
1881+
17011882
#else
17021883
#define BEGIN_LINE "# BEGIN GIT MAINTENANCE SCHEDULE"
17031884
#define END_LINE "# END GIT MAINTENANCE SCHEDULE"

t/t7900-maintenance.sh

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -367,7 +367,7 @@ test_expect_success 'register and unregister' '
367367
test_cmp before actual
368368
'
369369

370-
test_expect_success !MACOS_MAINTENANCE 'start from empty cron table' '
370+
test_expect_success !MACOS_MAINTENANCE,!MINGW 'start from empty cron table' '
371371
GIT_TEST_CRONTAB="test-tool crontab cron.txt" git maintenance start &&
372372
373373
# start registers the repo
@@ -378,7 +378,7 @@ test_expect_success !MACOS_MAINTENANCE 'start from empty cron table' '
378378
grep "for-each-repo --config=maintenance.repo maintenance run --schedule=weekly" cron.txt
379379
'
380380

381-
test_expect_success !MACOS_MAINTENANCE 'stop from existing schedule' '
381+
test_expect_success !MACOS_MAINTENANCE,!MINGW 'stop from existing schedule' '
382382
GIT_TEST_CRONTAB="test-tool crontab cron.txt" git maintenance stop &&
383383
384384
# stop does not unregister the repo
@@ -389,7 +389,7 @@ test_expect_success !MACOS_MAINTENANCE 'stop from existing schedule' '
389389
test_must_be_empty cron.txt
390390
'
391391

392-
test_expect_success !MACOS_MAINTENANCE 'start preserves existing schedule' '
392+
test_expect_success !MACOS_MAINTENANCE,!MINGW 'start preserves existing schedule' '
393393
echo "Important information!" >cron.txt &&
394394
GIT_TEST_CRONTAB="test-tool crontab cron.txt" git maintenance start &&
395395
grep "Important information!" cron.txt
@@ -441,6 +441,36 @@ test_expect_success MACOS_MAINTENANCE 'start and stop macOS maintenance' '
441441
test_cmp expect args
442442
'
443443

444+
test_expect_success MINGW 'start and stop Windows maintenance' '
445+
write_script print-args <<-\EOF &&
446+
echo $* >>args
447+
EOF
448+
449+
rm -f args &&
450+
GIT_TEST_CRONTAB="/bin/sh print-args" git maintenance start &&
451+
452+
# start registers the repo
453+
git config --get --global maintenance.repo "$(pwd)" &&
454+
455+
for frequency in hourly daily weekly
456+
do
457+
printf "/create /tn Git Maintenance (%s) /f /xml .git/objects/schedule-%s.xml\n" \
458+
$frequency $frequency
459+
done >expect &&
460+
test_cmp expect args &&
461+
462+
rm -f args &&
463+
GIT_TEST_CRONTAB="/bin/sh print-args" git maintenance stop &&
464+
465+
# stop does not unregister the repo
466+
git config --get --global maintenance.repo "$(pwd)" &&
467+
468+
rm expect &&
469+
printf "/delete /tn Git Maintenance (%s) /f\n" \
470+
hourly daily weekly >expect &&
471+
test_cmp expect args
472+
'
473+
444474
test_expect_success 'register preserves existing strategy' '
445475
git config maintenance.strategy none &&
446476
git maintenance register &&

0 commit comments

Comments
 (0)