fix: a secondary profile no longer removes the default profile's bare-named gateway launchers (review follow-up)
The bare `Hermes_Gateway` Scheduled Task and Startup entry are the live identity of the default ~/.hermes profile, yet any suffixed home classified them as its own pre-suffix strays, so `hermes -p work gateway uninstall` / `install --force` deleted the default profile's autostart. A bare-named Startup entry or task now counts as legacy only when its action targets THIS home's gateway-service dir (Startup file contents / `schtasks /Query /XML`); files inside this home's gateway-service dir are ours by construction. Invariant test (red before): sibling launchers survive uninstall with no /Delete issued. The windows_only live test now registers a task whose action targets the temp home so it still exercises the positive path.
This commit is contained in:
@@ -20,9 +20,20 @@ def _w():
|
||||
return gateway_windows
|
||||
|
||||
|
||||
def _targets_this_home(text: str) -> bool:
|
||||
"""A bare-named launcher is OURS only when its action points into THIS home's ``gateway-service``
|
||||
dir. The bare name is also the live identity of the default ``~/.hermes`` profile, so a secondary
|
||||
profile that classified every ``Hermes_Gateway`` object as its own stray would delete the default
|
||||
profile's gateway autostart on ``uninstall`` / ``install --force``."""
|
||||
w = _w()
|
||||
marker = w._normalize_windows_path(str(w._hermes_home() / "gateway-service"))
|
||||
return marker in w._normalize_windows_path(text)
|
||||
|
||||
|
||||
def legacy_launcher_artifacts() -> list[tuple[str, str, Path | str]]:
|
||||
"""``(kind, label, target)`` for every pre-suffix launcher still present; ``kind`` is ``"file"``
|
||||
or ``"task"``. Empty when this home owns the bare name (its current objects ARE the bare ones)."""
|
||||
"""``(kind, label, target)`` for every pre-suffix launcher of THIS home still present; ``kind`` is
|
||||
``"file"`` or ``"task"``. Empty when this home owns the bare name (its current objects ARE the bare
|
||||
ones). Bare-named objects belonging to another home (a sibling install) are left alone."""
|
||||
w = _w()
|
||||
bare = w._TASK_NAME_DEFAULT
|
||||
if w.get_task_name() == bare:
|
||||
@@ -33,13 +44,20 @@ def legacy_launcher_artifacts() -> list[tuple[str, str, Path | str]]:
|
||||
for path, label in (
|
||||
(startup / f"{bare}.vbs", "legacy pre-suffix Windows login item"),
|
||||
(startup / f"{bare}.cmd", "legacy pre-suffix Windows login item"),
|
||||
):
|
||||
try:
|
||||
if _targets_this_home(path.read_text(encoding="utf-8", errors="replace")):
|
||||
found.append(("file", label, path))
|
||||
except OSError:
|
||||
continue
|
||||
for path, label in (
|
||||
(service_dir / f"{bare}.vbs", "legacy pre-suffix task launcher"),
|
||||
(service_dir / f"{bare}.cmd", "legacy pre-suffix task script"),
|
||||
):
|
||||
if path.exists():
|
||||
if path.exists(): # inside this home by construction
|
||||
found.append(("file", label, path))
|
||||
code, _out, _err = w._exec_schtasks(["/Query", "/TN", bare])
|
||||
if code == 0:
|
||||
code, out, _err = w._exec_schtasks(["/Query", "/TN", bare, "/XML"])
|
||||
if code == 0 and _targets_this_home(out):
|
||||
found.append(("task", "legacy pre-suffix Scheduled Task", bare))
|
||||
return found
|
||||
|
||||
|
||||
@@ -448,11 +448,12 @@ def test_status_names_and_uninstall_removes_pre_suffix_launchers(monkeypatch, tm
|
||||
(home / "gateway-service").mkdir(parents=True)
|
||||
startup.mkdir()
|
||||
legacy_vbs = startup / "Hermes_Gateway.vbs"
|
||||
legacy_vbs.write_text("legacy", encoding="utf-8")
|
||||
legacy_vbs.write_text(gateway_windows._build_startup_launcher(home / "gateway-service" / "Hermes_Gateway.cmd"), encoding="utf-8")
|
||||
legacy_pair = home / "gateway-service" / "Hermes_Gateway.cmd"
|
||||
legacy_pair.write_text("legacy", encoding="utf-8")
|
||||
schtasks_calls = []
|
||||
registered = {"Hermes_Gateway"}
|
||||
task_xml = gateway_windows._build_scheduled_task_xml("Hermes_Gateway", home / "gateway-service" / "Hermes_Gateway.vbs", None)
|
||||
|
||||
def fake_schtasks(args):
|
||||
schtasks_calls.append(args)
|
||||
@@ -460,7 +461,7 @@ def test_status_names_and_uninstall_removes_pre_suffix_launchers(monkeypatch, tm
|
||||
if args[0] == "/Delete":
|
||||
registered.discard(name)
|
||||
return (0, "SUCCESS", "")
|
||||
return (0, "", "") if name in registered else (1, "", "ERROR: The system cannot find the file specified.")
|
||||
return (0, task_xml, "") if name in registered else (1, "", "ERROR: The system cannot find the file specified.")
|
||||
|
||||
monkeypatch.setattr(gateway_windows, "_assert_windows", lambda: None)
|
||||
monkeypatch.setattr(gateway_windows, "get_task_name", lambda: "Hermes_Gateway_alice")
|
||||
@@ -488,6 +489,44 @@ def test_status_names_and_uninstall_removes_pre_suffix_launchers(monkeypatch, tm
|
||||
assert "legacy pre-suffix" not in capsys.readouterr().out
|
||||
|
||||
|
||||
def test_secondary_profile_leaves_default_profiles_bare_launchers_alone(monkeypatch, tmp_path, capsys):
|
||||
"""The bare ``Hermes_Gateway`` task and Startup entry are the LIVE identity of the default ``~/.hermes``
|
||||
profile. From a secondary profile they are a sibling install, not this home's pre-suffix stray:
|
||||
``uninstall`` / ``install --force`` must issue no ``schtasks /Delete`` and unlink nothing."""
|
||||
startup, home, default_home = tmp_path / "Startup", tmp_path / "profiles" / "work", tmp_path / "default"
|
||||
(home / "gateway-service").mkdir(parents=True)
|
||||
(default_home / "gateway-service").mkdir(parents=True)
|
||||
startup.mkdir()
|
||||
default_vbs = startup / "Hermes_Gateway.vbs"
|
||||
default_vbs.write_text(gateway_windows._build_startup_launcher(default_home / "gateway-service" / "Hermes_Gateway.cmd"), encoding="utf-8")
|
||||
task_xml = gateway_windows._build_scheduled_task_xml("Hermes_Gateway", default_home / "gateway-service" / "Hermes_Gateway.vbs", None)
|
||||
schtasks_calls = []
|
||||
|
||||
def fake_schtasks(args):
|
||||
schtasks_calls.append(args)
|
||||
if args[0] == "/Query" and args[args.index("/TN") + 1] == "Hermes_Gateway":
|
||||
return (0, task_xml, "")
|
||||
return (1, "", "ERROR: The system cannot find the file specified.")
|
||||
|
||||
monkeypatch.setattr(gateway_windows, "_assert_windows", lambda: None)
|
||||
monkeypatch.setattr(gateway_windows, "get_task_name", lambda: "Hermes_Gateway_work")
|
||||
monkeypatch.setattr(gateway_windows, "get_task_script_path", lambda: home / "gateway-service" / "Hermes_Gateway_work.cmd")
|
||||
monkeypatch.setattr(gateway_windows, "get_startup_entry_path", lambda: startup / "Hermes_Gateway_work.vbs")
|
||||
monkeypatch.setattr(gateway_windows, "_legacy_startup_entry_path", lambda: startup / "Hermes_Gateway_work.cmd")
|
||||
monkeypatch.setattr(gateway_windows, "_startup_dir", lambda: startup)
|
||||
monkeypatch.setattr(gateway_windows, "_hermes_home", lambda: home)
|
||||
monkeypatch.setattr(gateway_windows, "_exec_schtasks", fake_schtasks)
|
||||
monkeypatch.setattr(gateway_windows, "_gateway_pids", lambda *a, **k: [])
|
||||
monkeypatch.setattr(gateway_windows, "_print_start_attestation_warning", lambda: None)
|
||||
|
||||
gateway_windows.status()
|
||||
assert "legacy pre-suffix" not in capsys.readouterr().out
|
||||
gateway_windows.uninstall()
|
||||
capsys.readouterr()
|
||||
assert default_vbs.exists()
|
||||
assert not any(call[0] == "/Delete" and "Hermes_Gateway" in call for call in schtasks_calls)
|
||||
|
||||
|
||||
# Reporter's `Export-ScheduledTask` of a task registered before the hardened template (#113670).
|
||||
_PRE_HARDENING_TASK_XML = """<?xml version="1.0" encoding="UTF-16"?>
|
||||
<Task version="1.3" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
|
||||
|
||||
@@ -28,14 +28,16 @@ def test_status_warns_and_uninstall_removes_pre_suffix_launchers(tmp_path, monke
|
||||
|
||||
current = gateway_windows.get_task_name()
|
||||
assert current != "Hermes_Gateway", "a temp home must carry a profile suffix"
|
||||
legacy_vbs = startup / "Hermes_Gateway.vbs"
|
||||
legacy_vbs.write_text("' pre-suffix login item\r\n", encoding="utf-8")
|
||||
legacy_pair = home / "gateway-service" / "Hermes_Gateway.vbs"
|
||||
legacy_pair.write_text("' pre-suffix launcher\r\n", encoding="utf-8")
|
||||
# Both objects must point INTO this home: a bare-named task/entry targeting another home is a
|
||||
# sibling install (the default profile's live gateway) and is deliberately left alone.
|
||||
legacy_vbs = startup / "Hermes_Gateway.vbs"
|
||||
legacy_vbs.write_text(gateway_windows._build_startup_launcher(legacy_pair.with_suffix(".cmd")), encoding="utf-8")
|
||||
|
||||
schtasks = shutil.which("schtasks") or "schtasks"
|
||||
create = subprocess.run(
|
||||
[schtasks, "/Create", "/F", "/TN", "Hermes_Gateway", "/SC", "ONLOGON", "/TR", "cmd.exe /c exit 0"],
|
||||
[schtasks, "/Create", "/F", "/TN", "Hermes_Gateway", "/SC", "ONLOGON", "/TR", f"wscript.exe //B {legacy_pair}"],
|
||||
capture_output=True, text=True, timeout=60,
|
||||
)
|
||||
assert create.returncode == 0, create.stderr
|
||||
|
||||
Reference in New Issue
Block a user