From 1edaeb8b76f6b872a6c810d404c944caf1a594b2 Mon Sep 17 00:00:00 2001 From: Parideboy Date: Wed, 12 Aug 2026 07:10:23 +0200 Subject: [PATCH] fix(install/windows): register persistent-task from S4U hidden XML (#2453) (#2459) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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 `true`, 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 /XML /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 S4U and true 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 --- headroom/install/supervisors.py | 135 ++++++++++++++++++++----- tests/test_install/test_supervisors.py | 51 +++++++--- 2 files changed, 146 insertions(+), 40 deletions(-) diff --git a/headroom/install/supervisors.py b/headroom/install/supervisors.py index 7d825fbe7..83f517576 100644 --- a/headroom/install/supervisors.py +++ b/headroom/install/supervisors.py @@ -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 = ( + " S-1-5-18\n" + " ServiceAccount\n" + " HighestAvailable" + ) + else: + principal = ( + f" {_xml_escape(_windows_current_user())}\n" + " S4U\n" + " LeastPrivilege" + ) + return ( + '\n' + '\n' + " \n" + f"{trigger_xml}\n" + " \n" + ' \n \n' + f"{principal}\n" + " \n \n" + " \n" + " true\n" + " IgnoreNew\n" + " false\n" + " false\n" + " PT0S\n" + " true\n" + " \n" + ' \n' + f" \n {_xml_escape(command)}\n \n" + " \n" + "\n" + ) + + +def _windows_boot_trigger() -> str: + return " \n true\n " + + +def _windows_health_trigger() -> str: + start = datetime.now().strftime("%Y-%m-%dT%H:%M:%S") + return ( + " \n" + f" {start}\n" + " true\n" + " \n" + " PT5M\n" + " false\n" + " \n" + " " + ) + + +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), diff --git a/tests/test_install/test_supervisors.py b/tests/test_install/test_supervisors.py index 15a105bea..60d1f85b1 100644 --- a/tests/test_install/test_supervisors.py +++ b/tests/test_install/test_supervisors.py @@ -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 "S4U" in xml + assert "true" in xml + assert "PT5M" in xml + assert "C:\\tmp\\default\\ensure-headroom.cmd" 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 "S-1-5-18" in xml + assert "ServiceAccount" in xml + assert "" 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")