fix(install/windows): register persistent-task from S4U hidden XML (#2453) (#2459)

## Description

Windows `persistent-task` created its startup and 5-minute health tasks
via `schtasks` command-line flags, which register the task with an
**interactive-token** principal. Every task run spawned a visible
console window that briefly grabbed keyboard focus before vanishing —
every 5 minutes, indefinitely (and at boot / proxy restart). Fixes
#2453.

This registers the tasks from Task Scheduler **XML** instead: user-scope
tasks use an **S4U** principal (run whether the user is logged on or
not, no stored password) with `<Hidden>true</Hidden>`, so runs execute
in a non-interactive session and never draw a window. System-scope tasks
keep the LocalSystem service account (which already has no desktop).

## Type of Change

- [x] Bug fix (non-breaking change which fixes an issue)

## Changes Made

- `headroom/install/supervisors.py`: add `_windows_task_xml()`
(S4U/hidden for user scope, LocalSystem for system scope),
`_windows_boot_trigger()`, `_windows_health_trigger()` (PT5M
repetition), and `_register_windows_task()` (writes UTF-16 XML to a temp
file and calls `schtasks /Create /TN <n> /XML <file> /F`). Rewrite the
Windows TASK branch of `install_supervisor` to register both tasks from
XML.
- `tests/test_install/test_supervisors.py`: unit tests asserting the XML
carries `S4U` + `Hidden` + `PT5M` for user scope and `S-1-5-18` /
`ServiceAccount` for system scope; updated the install-flow assertion to
expect `schtasks /XML` registration for the startup and health tasks.

## Testing

- [x] Unit tests pass

```
$ python -m pytest tests/test_install/test_supervisors.py -q
collected 29 items
tests\test_install\test_supervisors.py .............................     [100%]
============================= 29 passed in 1.48s ==============================
```

## Real Behavior Proof

- Environment: Windows 11 Pro 10.0.26200, Python 3.13.11
- Exact command / steps: python -m pytest
tests/test_install/test_supervisors.py -q; ruff check + ruff format
--check; mypy headroom/install/supervisors.py --ignore-missing-imports
- Observed result: 29 passed; ruff clean; mypy exit 0. Generated XML
contains <LogonType>S4U</LogonType> and <Hidden>true</Hidden> for user
scope.
- Not tested: live end-to-end `headroom install apply --preset
persistent-task` on a physical desktop confirming zero console flash
over a >5-minute window (no interactive Windows session in CI).

## Review Readiness

- [x] I have performed a self-review
- [x] This PR is ready for human review

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Parideboy
2026-08-12 07:10:23 +02:00
committed by GitHub
parent 2b5ee7cde8
commit 1edaeb8b76
2 changed files with 146 additions and 40 deletions
+108 -27
View File
@@ -2,13 +2,17 @@
from __future__ import annotations
import getpass
import os
import re
import shlex
import subprocess
import sys
import tempfile
import time
from datetime import datetime
from pathlib import Path
from xml.sax.saxutils import escape as _xml_escape
import click
@@ -242,6 +246,97 @@ def _linux_task_spec(manifest: DeploymentManifest, ensure_script: Path) -> tuple
return None, content
def _windows_current_user() -> str:
"""Best-effort ``DOMAIN\\USER`` for the S4U task principal."""
user = os.environ.get("USERNAME") or getpass.getuser()
domain = os.environ.get("USERDOMAIN")
return f"{domain}\\{user}" if domain else user
def _windows_task_xml(command: str, *, trigger_xml: str, scope: str) -> str:
"""Render Task Scheduler XML that runs ``command`` without a visible window.
User-scope tasks use an S4U principal ("run whether user is logged on or
not", no stored password) so each run happens in a non-interactive session
and never draws a console window (issue #2453). System-scope tasks keep the
LocalSystem service account, which already has no desktop.
"""
if scope == "system":
principal = (
" <UserId>S-1-5-18</UserId>\n"
" <LogonType>ServiceAccount</LogonType>\n"
" <RunLevel>HighestAvailable</RunLevel>"
)
else:
principal = (
f" <UserId>{_xml_escape(_windows_current_user())}</UserId>\n"
" <LogonType>S4U</LogonType>\n"
" <RunLevel>LeastPrivilege</RunLevel>"
)
return (
'<?xml version="1.0" encoding="UTF-16"?>\n'
'<Task version="1.2" '
'xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">\n'
" <Triggers>\n"
f"{trigger_xml}\n"
" </Triggers>\n"
' <Principals>\n <Principal id="Author">\n'
f"{principal}\n"
" </Principal>\n </Principals>\n"
" <Settings>\n"
" <Hidden>true</Hidden>\n"
" <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>\n"
" <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>\n"
" <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>\n"
" <ExecutionTimeLimit>PT0S</ExecutionTimeLimit>\n"
" <StartWhenAvailable>true</StartWhenAvailable>\n"
" </Settings>\n"
' <Actions Context="Author">\n'
f" <Exec>\n <Command>{_xml_escape(command)}</Command>\n </Exec>\n"
" </Actions>\n"
"</Task>\n"
)
def _windows_boot_trigger() -> str:
return " <BootTrigger>\n <Enabled>true</Enabled>\n </BootTrigger>"
def _windows_health_trigger() -> str:
start = datetime.now().strftime("%Y-%m-%dT%H:%M:%S")
return (
" <TimeTrigger>\n"
f" <StartBoundary>{start}</StartBoundary>\n"
" <Enabled>true</Enabled>\n"
" <Repetition>\n"
" <Interval>PT5M</Interval>\n"
" <StopAtDurationEnd>false</StopAtDurationEnd>\n"
" </Repetition>\n"
" </TimeTrigger>"
)
def _register_windows_task(name: str, xml: str) -> None:
"""Register ``xml`` as scheduled task ``name`` via ``schtasks /XML``."""
# schtasks reads the XML from a file; UTF-16 matches the declared encoding.
tmp = tempfile.NamedTemporaryFile(mode="w", suffix=".xml", encoding="utf-16", delete=False)
try:
tmp.write(xml)
tmp.close()
subprocess.run(
["schtasks", "/Create", "/TN", name, "/XML", tmp.name, "/F"],
check=True,
)
finally:
try:
os.unlink(tmp.name)
except OSError:
pass
def install_supervisor(manifest: DeploymentManifest) -> list[ArtifactRecord]:
"""Install service/task artifacts for the deployment."""
@@ -345,35 +440,21 @@ def install_supervisor(manifest: DeploymentManifest) -> list[ArtifactRecord]:
startup_name = f"{manifest.service_name}-startup"
health_name = f"{manifest.service_name}-health"
startup_cmd = str(windows_ensure_cmd_path(manifest.profile))
user_args = ["/RU", "SYSTEM"] if manifest.scope == "system" else []
start_schedule = [
"schtasks",
"/Create",
"/TN",
# Register from task XML (not schtasks flags) so the principal is S4U /
# hidden — flag-created tasks use an interactive token and flash a
# focus-stealing console on every run (issue #2453).
_register_windows_task(
startup_name,
"/TR",
startup_cmd,
"/SC",
"ONSTART",
"/F",
*user_args,
]
health_schedule = [
"schtasks",
"/Create",
"/TN",
_windows_task_xml(
startup_cmd, trigger_xml=_windows_boot_trigger(), scope=manifest.scope
),
)
_register_windows_task(
health_name,
"/TR",
startup_cmd,
"/SC",
"MINUTE",
"/MO",
"5",
"/F",
*user_args,
]
subprocess.run(start_schedule, check=True)
subprocess.run(health_schedule, check=True)
_windows_task_xml(
startup_cmd, trigger_xml=_windows_health_trigger(), scope=manifest.scope
),
)
records.extend(
[
ArtifactRecord(kind="windows-task", path=startup_name),
+38 -13
View File
@@ -13,6 +13,9 @@ from headroom.install.supervisors import (
_macos_launchd_plist,
_render_unix_runner,
_render_windows_runner,
_windows_boot_trigger,
_windows_health_trigger,
_windows_task_xml,
install_supervisor,
remove_supervisor,
render_runner_scripts,
@@ -21,6 +24,31 @@ from headroom.install.supervisors import (
)
def test_windows_task_xml_user_scope_is_hidden_s4u() -> None:
# #2453: user-scope tasks must run S4U (non-interactive, no window) and
# hidden so the 5-minute health run never steals keyboard focus.
xml = _windows_task_xml(
"C:\\tmp\\default\\ensure-headroom.cmd",
trigger_xml=_windows_health_trigger(),
scope="user",
)
assert "<LogonType>S4U</LogonType>" in xml
assert "<Hidden>true</Hidden>" in xml
assert "<Interval>PT5M</Interval>" in xml
assert "<Command>C:\\tmp\\default\\ensure-headroom.cmd</Command>" in xml
def test_windows_task_xml_system_scope_uses_localsystem() -> None:
xml = _windows_task_xml(
"C:\\tmp\\default\\ensure-headroom.cmd",
trigger_xml=_windows_boot_trigger(),
scope="system",
)
assert "<UserId>S-1-5-18</UserId>" in xml
assert "<LogonType>ServiceAccount</LogonType>" in xml
assert "<BootTrigger>" in xml
def _manifest(
*,
profile: str = "default",
@@ -377,19 +405,16 @@ def test_install_supervisor_darwin_windows_and_unsupported(monkeypatch, tmp_path
"sc.exe create headroom-default "
'binPath= "cmd.exe /c \\"C:\\tmp\\default\\run-headroom.cmd\\"" start= auto'
) in calls
assert [
"schtasks",
"/Create",
"/TN",
"headroom-default-health",
"/TR",
"C:\\tmp\\default\\ensure-headroom.cmd",
"/SC",
"MINUTE",
"/MO",
"5",
"/F",
] in calls
# #2453: tasks are registered from S4U/hidden XML via `schtasks /XML`, not
# interactive-token flag creation. Assert the startup and health tasks are
# each created from an XML file (the temp path varies).
task_creates = [
c for c in calls if isinstance(c, list) and c[:2] == ["schtasks", "/Create"] and "/XML" in c
]
created_names = {c[c.index("/TN") + 1] for c in task_creates}
assert {"headroom-default-startup", "headroom-default-health"} <= created_names
for c in task_creates:
assert c[-1] == "/F"
monkeypatch.setattr("headroom.install.supervisors.sys.platform", "plan9")
monkeypatch.setattr("headroom.install.supervisors.sys.platform", "plan9")