aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-node')
-rw-r--r--packages/meshbay-node/src/meshbay_node/platform.py58
-rw-r--r--packages/meshbay-node/tests/test_packaging_win.py50
-rw-r--r--packages/meshbay-node/tests/test_platform.py43
3 files changed, 114 insertions, 37 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/platform.py b/packages/meshbay-node/src/meshbay_node/platform.py
index 981d466..7e840ad 100644
--- a/packages/meshbay-node/src/meshbay_node/platform.py
+++ b/packages/meshbay-node/src/meshbay_node/platform.py
@@ -367,12 +367,31 @@ def autostart_end() -> None:
#
# The middle ground: a Scheduled Task, created once with admin rights, that
# runs *as this user* at system boot without needing them to sign in first.
-# `schtasks /create ... /ru <user> /rp ""` with no `/it` registers an S4U
-# (Service For User) logon — no password stored anywhere, and unlike
-# LocalSystem it loads this account's own profile, so config_dir()/data_dir()
-# need no special-casing at all. The cost: S4U carries no *network* credential
-# (no reaching a domain share as this user), which the node never needed
-# anyway — everything it touches is local disk plus outbound internet.
+# That requires an S4U (Service For User) logon: no password stored anywhere,
+# and unlike LocalSystem it loads this account's own profile, so
+# config_dir()/data_dir() need no special-casing at all. The cost: S4U
+# carries no *network* credential (no reaching a domain share as this user),
+# which the node never needed anyway — everything it touches is local disk
+# plus outbound internet.
+#
+# Getting S4U out of raw `schtasks.exe /create` means inferring it from
+# whether `/rp` is present and what it holds -- undocumented, and it went
+# wrong twice on this exact machine's blank-password account (common on a
+# personal PC — confirmed via `net user`, "Password required: No"):
+# `/rp ""` routes through credential validation, which Windows' default
+# policy blocks for a blank password ("WARNING: When the run-as password is
+# empty..." then "ERROR: The user name or password is incorrect.", 2026-09-05
+# repro); omitting `/rp` entirely does get past that, but registers
+# `Logon Mode: Interactive only` instead of S4U — confirmed live the same
+# day: the task never ran at boot, and manually running it while signed in
+# still failed (`Last Result: -2147024894`, no process ever launched).
+# `Register-ScheduledTask` from the `ScheduledTasks` PowerShell module takes
+# `-LogonType S4U` as a named, explicit value — no inference, no ambiguity —
+# so that is what actually creates the task, shelling out to `powershell.exe`
+# instead of `schtasks.exe` for this one call. Still unverified end-to-end
+# past "the syntax runs and denies access when not elevated" (2026-09-05) —
+# needs a real elevated install + reboot to confirm S4U registers as such and
+# the task actually launches the process.
#
# Creating the task needs admin (a boot-trigger touches system-wide scheduler
# state, the same reason /sc onlogon did — see the autostart section above).
@@ -415,9 +434,9 @@ def service_status() -> dict:
def service_install(exe: str | None = None) -> None:
"""
- Register the boot-time Scheduled Task. Needs admin — raises RuntimeError
- with schtasks' own message on failure, which is "Access is denied." when
- not elevated.
+ Register the boot-time Scheduled Task with an S4U logon. Needs admin —
+ raises RuntimeError with Register-ScheduledTask's own message on failure,
+ which is "Access is denied." when not elevated.
Removes the per-user Startup launcher first, if present: the two
mechanisms are mutually exclusive by design (both installed would start
@@ -437,10 +456,25 @@ def service_install(exe: str | None = None) -> None:
user = _current_user()
if not user:
raise RuntimeError("could not determine the current user (USERNAME unset)")
- r = _schtasks("/create", "/tn", TASK_NAME, "/tr", f'"{exe}"',
- "/sc", "onstart", "/ru", user, "/rp", "", "/rl", "limited", "/f")
+ # Passed via the environment, not interpolated into the -Command string,
+ # so a path or username containing a quote or $ can't break the script.
+ env = {**os.environ, "MESHBAY_SVC_EXE": exe, "MESHBAY_SVC_USER": user,
+ "MESHBAY_SVC_TASK": TASK_NAME}
+ ps_script = (
+ "$ErrorActionPreference = 'Stop'; "
+ "$a = New-ScheduledTaskAction -Execute $env:MESHBAY_SVC_EXE; "
+ "$t = New-ScheduledTaskTrigger -AtStartup; "
+ "$p = New-ScheduledTaskPrincipal -UserId $env:MESHBAY_SVC_USER "
+ "-LogonType S4U -RunLevel Limited; "
+ "Register-ScheduledTask -TaskName $env:MESHBAY_SVC_TASK "
+ "-Action $a -Trigger $t -Principal $p -Force | Out-Null"
+ )
+ r = subprocess.run(
+ ["powershell", "-NoProfile", "-NonInteractive", "-Command", ps_script],
+ capture_output=True, text=True, env=env)
if r.returncode != 0:
- raise RuntimeError(f"schtasks /create failed: {r.stderr.strip() or r.stdout.strip()}")
+ raise RuntimeError(
+ f"Register-ScheduledTask failed: {r.stderr.strip() or r.stdout.strip()}")
def service_remove() -> None:
diff --git a/packages/meshbay-node/tests/test_packaging_win.py b/packages/meshbay-node/tests/test_packaging_win.py
index 0cf6367..df5a24f 100644
--- a/packages/meshbay-node/tests/test_packaging_win.py
+++ b/packages/meshbay-node/tests/test_packaging_win.py
@@ -284,29 +284,39 @@ def test_service_ps1_and_service_mode_ps1_are_extraresources():
def test_service_install_uses_s4u_not_a_stored_password():
"""
- schtasks /create ... /ru <user> /rp "" with no /it registers an S4U logon:
- no password stored anywhere, and — unlike LocalSystem/NetworkService — it
- loads this account's own profile, so %LOCALAPPDATA%\\meshbay\\ needs no
- relocation. Losing the empty /rp "" (e.g. "fixing" it into a real prompt
- for a password) would either store a secret or silently stop working.
+ Register-ScheduledTask -LogonType S4U: no password stored anywhere, and
+ — unlike LocalSystem/NetworkService — it loads this account's own
+ profile, so %LOCALAPPDATA%\\meshbay\\ needs no relocation.
+
+ Not `schtasks /create`: schtasks only *infers* the logon type from
+ whether /rp is present, and both readings broke live on a blank-password
+ account (common on a personal PC, 2026-09-05) -- `/rp ""` fails
+ credential validation ("the user name or password is incorrect", even
+ though nothing is wrong), and omitting /rp registers "Interactive only"
+ instead of S4U, which never runs at boot and does not launch anything
+ even run on demand while signed in. -LogonType S4U is explicit, so losing
+ it (e.g. "simplifying" back to schtasks, or dropping -ErrorAction Stop so
+ a permission failure silently falls through to "installed") would
+ reintroduce one of those two live-reproduced failures.
"""
src = (WIN / "service.ps1").read_text(encoding="utf-8")
- # The prose above the actual command is allowed to say "/it" while
- # explaining why it is absent (the same "read the comment, not the
- # directive" trap CLAUDE.md already tracks) -- so check the real
- # invocation line, not the whole file.
- create_line = next(
- (line for line in src.splitlines() if line.strip().startswith("& schtasks")
- and "/create" in line), None)
- assert create_line, "no schtasks /create invocation found"
- tokens = create_line.split()
- assert "/sc" in tokens and tokens[tokens.index("/sc") + 1] == "onstart", (
- "must trigger at boot, not at sign-in (/sc onlogon)")
- assert "/ru" in tokens
- assert "/rp" in tokens and tokens[tokens.index("/rp") + 1] == '""', (
- "must pass an empty run-as password (S4U)")
- assert "/it" not in tokens, "an interactive-token task would need the user signed in"
+ # Isolate the actual "install" case from the docstring above it (which
+ # names these same cmdlets in prose) -- checks below must see only code.
+ lines = src.splitlines()
+ install_start = next(i for i, line in enumerate(lines) if '"install" {' in line)
+ remove_start = next(i for i, line in enumerate(lines) if '"remove" {' in line)
+ install_block = "\n".join(lines[install_start:remove_start])
+
+ assert "New-ScheduledTaskTrigger -AtStartup" in install_block, (
+ "must trigger at boot, not at sign-in")
+ assert "-LogonType S4U" in install_block, (
+ "must request S4U explicitly, not infer it from /rp")
+ assert "Register-ScheduledTask" in install_block
+ assert "-ErrorAction Stop" in install_block, (
+ "a permission failure must throw, not fall through as if it installed")
assert "Get-Credential" not in src, "no password should ever be prompted for"
+ assert "schtasks" not in install_block, (
+ "the install branch must not fall back to schtasks /create")
def test_service_task_name_is_the_same_everywhere():
diff --git a/packages/meshbay-node/tests/test_platform.py b/packages/meshbay-node/tests/test_platform.py
index 91713be..92e74df 100644
--- a/packages/meshbay-node/tests/test_platform.py
+++ b/packages/meshbay-node/tests/test_platform.py
@@ -346,24 +346,57 @@ def test_service_install_removes_the_startup_launcher_first(win_startup, monkeyp
monkeypatch.setattr(plat, "_current_user", lambda: "DOMAIN\\user")
calls = []
monkeypatch.setattr(
- plat, "_schtasks",
- lambda *args: calls.append(args) or Mock(returncode=0, stdout="", stderr=""))
+ plat.subprocess, "run",
+ lambda argv, **kw: calls.append((argv, kw)) or Mock(returncode=0, stdout="", stderr=""))
plat.service_install(exe=r"C:\x\meshbay-node.exe")
assert not win_startup.exists() # removed as part of service_install
- assert calls and calls[0][0] == "/create"
+ assert calls and calls[0][0][0] == "powershell"
+ env = calls[0][1]["env"]
+ assert env["MESHBAY_SVC_USER"] == "DOMAIN\\user"
+ assert env["MESHBAY_SVC_EXE"] == r"C:\x\meshbay-node.exe"
+
+
+def test_service_install_uses_s4u_not_a_stored_password(monkeypatch):
+ """Register-ScheduledTask -LogonType S4U, not schtasks: schtasks only
+ infers the logon type from whether /rp is present, and both readings
+ broke live on a blank-password account (2026-09-05) -- /rp "" fails
+ credential validation, and omitting /rp registers "Interactive only",
+ which never runs at boot or on demand. See platform.py's service mode
+ comment for the full story."""
+ monkeypatch.setattr(plat, "_current_user", lambda: "DOMAIN\\user")
+ calls = []
+ monkeypatch.setattr(
+ plat.subprocess, "run",
+ lambda argv, **kw: calls.append((argv, kw)) or Mock(returncode=0, stdout="", stderr=""))
+
+ plat.service_install(exe=r"C:\x\meshbay-node.exe")
+
+ script = calls[0][0][-1]
+ assert "-LogonType S4U" in script
+ assert "New-ScheduledTaskTrigger -AtStartup" in script
def test_service_install_tolerates_no_startup_launcher_present(win_startup, monkeypatch):
assert not win_startup.exists()
monkeypatch.setattr(plat, "_current_user", lambda: "DOMAIN\\user")
- monkeypatch.setattr(plat, "_schtasks",
- lambda *args: Mock(returncode=0, stdout="", stderr=""))
+ monkeypatch.setattr(plat.subprocess, "run",
+ lambda argv, **kw: Mock(returncode=0, stdout="", stderr=""))
plat.service_install(exe=r"C:\x\meshbay-node.exe") # no error
assert not win_startup.exists()
+def test_service_install_raises_with_powershells_error_message(monkeypatch):
+ monkeypatch.setattr(plat, "_current_user", lambda: "DOMAIN\\user")
+ monkeypatch.setattr(
+ plat.subprocess, "run",
+ lambda argv, **kw: Mock(returncode=1, stdout="", stderr="Access is denied."))
+
+ with pytest.raises(RuntimeError, match="Access is denied"):
+ plat.service_install(exe=r"C:\x\meshbay-node.exe")
+
+
# ── Packaged defaults ────────────────────────────────────────────────────────
def test_frozen_build_finds_default_env_beside_the_executable(monkeypatch, tmp_path):