Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions hermes_cli/gateway_windows.py
Original file line number Diff line number Diff line change
Expand Up @@ -484,13 +484,63 @@ def _install_scheduled_task(task_name: str, script_path: Path) -> tuple[bool, st
for argv in variants:
code, out, err = _exec_schtasks(argv)
if code == 0:
limit_ok, limit_detail = _disable_scheduled_task_time_limit(task_name)
if not limit_ok:
return (
False,
f"Created Scheduled Task {task_name!r}, but failed to disable the default "
f"72-hour time limit: {limit_detail}",
)
return (True, f"Created Scheduled Task {task_name!r}")
last_code, last_err = code, (err or out or "")
if delete_detail and "cannot find" not in delete_detail.lower():
last_err = f"{last_err.strip()} (delete detail: {delete_detail})"
return (False, f"schtasks /Create failed (code {last_code}): {last_err.strip()}")


def _disable_scheduled_task_time_limit(task_name: str) -> tuple[bool, str]:
"""Disable Windows Task Scheduler's default 72-hour execution time limit."""
_assert_windows()
powershell = shutil.which("powershell.exe") or shutil.which("pwsh.exe")
if powershell is None:
return (False, "PowerShell not found on PATH")

script = (
"$ErrorActionPreference = 'Stop'; "
"$task = Get-ScheduledTask -TaskName $env:HERMES_TASK_NAME; "
"$task.Settings.ExecutionTimeLimit = 'PT0S'; "
"Set-ScheduledTask -InputObject $task | Out-Null"
)
env = os.environ.copy()
env["HERMES_TASK_NAME"] = task_name
try:
proc = subprocess.run(
[
powershell,
"-NoProfile",
"-NonInteractive",
"-ExecutionPolicy",
"Bypass",
"-Command",
script,
],
capture_output=True,
text=True,
encoding=_schtasks_encoding(),
errors="replace",
timeout=_SCHTASKS_TIMEOUT_S,
creationflags=0x08000000, # CREATE_NO_WINDOW
env=env,
)
except subprocess.TimeoutExpired:
return (False, f"PowerShell timed out after {_SCHTASKS_TIMEOUT_S}s")
except OSError as e:
return (False, f"PowerShell invocation failed: {e}")

detail = (proc.stderr or proc.stdout or "").strip()
if proc.returncode != 0:
return (False, detail or f"PowerShell exited with code {proc.returncode}")
return (True, "ExecutionTimeLimit set to PT0S")


def _install_startup_entry(script_path: Path) -> Path:
Expand Down
58 changes: 58 additions & 0 deletions tests/hermes_cli/test_gateway_windows.py
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,11 @@ def test_install_scheduled_task_recreates_instead_of_change(monkeypatch, tmp_pat
script_path = tmp_path / "Hermes_Gateway_alice.cmd"

monkeypatch.setattr(gateway_windows, "_assert_windows", lambda: None)
monkeypatch.setattr(
gateway_windows,
"_disable_scheduled_task_time_limit",
lambda task_name: calls.append(("disable_limit", task_name)) or (True, "ok"),
)

def fake_schtasks(args):
calls.append(tuple(args))
Expand All @@ -259,6 +264,59 @@ def fake_schtasks(args):
assert calls[1][0] == "/Create"
assert "/SC" in calls[1]
assert "ONLOGON" in calls[1]
assert ("disable_limit", "Hermes_Gateway_alice") in calls


def test_install_scheduled_task_fails_if_time_limit_cannot_be_disabled(monkeypatch, tmp_path):
"""A gateway task with Windows' default 72h limit is not a valid install."""
script_path = tmp_path / "Hermes_Gateway_alice.cmd"

monkeypatch.setattr(gateway_windows, "_assert_windows", lambda: None)
monkeypatch.setattr(
gateway_windows,
"_exec_schtasks",
lambda args: (0, "SUCCESS", "") if args[0] in {"/Delete", "/Create"} else (1, "", "unexpected"),
)
monkeypatch.setattr(
gateway_windows,
"_disable_scheduled_task_time_limit",
lambda task_name: (False, "Set-ScheduledTask failed"),
)

ok, detail = gateway_windows._install_scheduled_task("Hermes_Gateway_alice", script_path)

assert ok is False
assert "72-hour time limit" in detail
assert "Set-ScheduledTask failed" in detail


def test_disable_scheduled_task_time_limit_sets_pt0s(monkeypatch):
"""PT0S disables the Task Scheduler execution time limit."""
captured: dict[str, object] = {}

class _FakeCompleted:
returncode = 0
stdout = ""
stderr = ""

def fake_run(cmd, **kwargs):
captured["cmd"] = cmd
captured.update(kwargs)
return _FakeCompleted()

monkeypatch.setattr(gateway_windows, "_assert_windows", lambda: None)
monkeypatch.setattr(gateway_windows.shutil, "which", lambda name: r"C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe")
monkeypatch.setattr(gateway_windows.subprocess, "run", fake_run)

ok, detail = gateway_windows._disable_scheduled_task_time_limit("Hermes_Gateway_alice")

assert ok is True
assert "PT0S" in detail
command = captured["cmd"]
assert isinstance(command, list)
assert "Set-ScheduledTask" in command[-1]
assert "ExecutionTimeLimit = 'PT0S'" in command[-1]
assert captured["env"]["HERMES_TASK_NAME"] == "Hermes_Gateway_alice"


def test_install_scheduled_task_success_start_now_uses_direct_spawn_not_task_run(monkeypatch, tmp_path, capsys):
Expand Down